Develop with Python on Ubuntu¶
This tutorial shows how to run, check, and debug Python scripts on Ubuntu. For instructions on how to install Python and related tooling, including IDEs, debuggers, and linters, see the dedicated guide on How to set up a development environment for Python on Ubuntu. This article assumes that tooling suggested in that article has been installed.
Important
To separate the system installation of Python from your development and testing setup, only use virtual environments for installing development project dependencies and running the developed code. This guide uses the standard venv virtual environment.
Preparing a Python virtual environment¶
(Optional) Create a directory for Python development, as well as a directory for the new project:
$ mkdir -p ~/python/helloworld $ cd ~/python/helloworld
Create a separate virtual environment for the new project (specifying
.venvas the directory for it):$ python3 -m venv .venv
Activate the virtual environment by sourcing the
activatescript:$ source .venv/bin/activate
Check that the environment has been set up:
dev@ubuntu:~/python/helloworld$which python3/home/dev/python/helloworld/.venv/bin/python3
Note
If you attempt to install a Python package using pip outside of a Python virtual environment, such as venv, on an Ubuntu (Debian) system, you receive a warning that explains that you should keep the system installation of Python (installed using .deb packages from Ubuntu repositories) separate from Python packages installed using pip:
$ which pip
/usr/bin/pip
$ pip install -r requirements.txt
error: externally-managed-environment
× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
python3-xyz, where xyz is the package you are trying to
install.
If you wish to install a non-Debian-packaged Python package,
create a virtual environment using python3 -m venv path/to/venv.
Then use path/to/venv/bin/python and path/to/venv/bin/pip. Make
sure you have python3-full installed.
If you wish to install a non-Debian packaged Python application,
it may be easiest to use pipx install xyz, which will manage a
virtual environment for you. Make sure you have pipx installed.
See /usr/share/doc/python3.12/README.venv for more information.
note: If you believe this is a mistake, please contact your Python
installation or OS distribution provider. You can override this,
at the risk of breaking your Python installation or OS, by passing
--break-system-packages.
hint: See PEP 668 for the detailed specification.
Creating a basic Python program¶
To illustrate the installation of a dependency confined to the Python virtual environment, the following example uses the requests library for handling HTTP requests.
Create a
requirements.txtfile with the list of dependencies. For example:$ echo "requests" > requirements.txt
Install the dependencies:
$ pip install -r requirements.txt
Checking the list of Python packages installed within the virtual environment should show output similar to this:
dev@ubuntu:~/python/helloworld$pip listPackage Version -------- ------- certifi 2025.1.31 charset-normalizer 3.4.1 idna 3.10 pip 24.2 requests 2.32.3 urllib3 2.4.0
(The version numbers can differ based on your environment.)
Write a Python script that uses the installed dependency. For example, create a
helloworld.pyfile with the following contents:helloworld.py¶import requests def hello_world(): # Use the example HTTP response service url = "https://httpbin.org/post" # Define a custom header for the POST method header = {"Message": "Hello, world!"} try: # Send the defined header to the response service response = requests.post(url, headers=header) # Basic error handling response.raise_for_status() # Parse the response response_data = response.json() # Print the message print(response_data["headers"]["Message"]) except requests.exceptions.RequestException as e: print(f"An error occurred: {e}") if __name__ == "__main__": hello_world()
Executing the program should result in the message you defined and sent using the
POSTmethod printed to standard output:dev@ubuntu:~/python/helloworld$python3 helloworld.pyHello, world!
Improving Python code with the help of tooling¶
Use linters and formatters to improve the quality and style of your Python code to achieve consistency and better readability.
In this example, we use the Flake8 code checker and Black formatter to identify areas for improvement and automatically format code. See How to set up a development environment for Python on Ubuntu for instructions on how to install these tools.
Checking Python code with Flake8¶
Consider the ‘Hello, world!’ script shown in Creating a basic Python program. Let’s introduce a simple style transgression into the code by deleting one of the blank lines after the hello_world() function:
helloworld.py¶ 1import requests
2
3
4def hello_world():
5 # Use the example HTTP response service
6 url = "https://httpbin.org/post"
7
8 # Define a custom header for the POST method
9 header = {"Message": "Hello, world!"}
10
11 try:
12 # Send the defined header to the response service
13 response = requests.post(url, headers=header)
14
15 # Basic error handling
16 response.raise_for_status()
17
18 # Parse the response
19 response_data = response.json()
20
21 # Print the message
22 print(response_data["headers"]["Message"])
23
24 except requests.exceptions.RequestException as e:
25 print(f"An error occurred: {e}")
26
27if __name__ == "__main__":
28 hello_world()
Running the Flake8 checker identifies the offense and informs us that it expects one more blank line on line number 27:
$ flake8 helloworld.py
helloworld.py:27:1: E305 expected 2 blank lines after class
or function definition, found 1Reformatting Python code with Black¶
Running the Black formatter automatically reformats the code and fixes the problem:
$ black helloworld.py
reformatted helloworld.py
All done! ✨ 🍰 ✨
1 file reformatted.Black is opinionated
Note that Black proceeds to reformat the code without asking for a confirmation.
Debugging Python code¶
To allow for the possibility of inspecting the state of the script at different points of execution, add breakpoints. In this example, we use the ipdb debugger, which is an enhanced version of the built-in pdb debugger.
Add
ipdbto the list of dependencies:$ echo "ipdb" >> requirements.txt
Install the dependencies:
$ pip install -r requirements.txt
Add
ipdbmodule import, and insert a breakpoint in the code (see line 23):helloworld.py¶1import requests 2import ipdb 3 4 5def hello_world(): 6 # Use the example HTTP response service 7 url = "https://httpbin.org/post" 8 9 # Define a custom header for the POST method 10 header = {"Message": "Hello, world!"} 11 12 try: 13 # Send the defined header to the response service 14 response = requests.post(url, headers=header) 15 16 # Basic error handling 17 response.raise_for_status() 18 19 # Parse the response 20 response_data = response.json() 21 22 # Set a breakpoint to check response data 23 ipdb.set_trace() 24 25 # Print the message 26 print(response_data["headers"]["Message"]) 27 28 except requests.exceptions.RequestException as e: 29 print(f"An error occurred: {e}") 30 31 32if __name__ == "__main__": 33 hello_world()
Execute the script to interact with the
ipdbdebugger:$ python3 helloworld.py > /home/dev/python/hw.py(26)hello_world() 25 # Print the message ---> 26 print(response_data["headers"]["Message"]) 27 ipdb> ipdb> pp response_data['headers'] {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Content-Length': '0', 'Host': 'httpbin.org', 'Message': 'Hello, world!', 'User-Agent': 'python-requests/2.32.3', 'X-Amzn-Trace-Id': 'Root=1-680a5186-49625c625bc31933473464b7'} ipdb> response_data['headers']['Message'] 'Hello, world!'
In the above example, we query the
response_datavariable using thepp(pretty print) command and then by specifying theMessageheader directly.
Testing Python code with a unit test¶
The following example shows how to use the pytest testing tool. First we implement a simple unit test that supplies mock data in place of querying the remote service and compares the output with the expected return value from the hello_world() function. Then we run pytest to perform the test.
Ensure that
pytestandpytest-mockare installed:$ apt install python3-pytest python3-pytest-mock
If you added debugging lines when Debugging Python code, comment them out:
helloworld.py¶1import requests 2# import ipdb 3 4 5def hello_world(): 6 # Use the example HTTP response service 7 url = "https://httpbin.org/post" 8 9 # Define a custom header for the POST method 10 header = {"Message": "Hello, world!"} 11 12 try: 13 # Send the defined header to the response service 14 response = requests.post(url, headers=header) 15 16 # Basic error handling 17 response.raise_for_status() 18 19 # Parse the response 20 response_data = response.json() 21 22 # # Set a breakpoint to check response data 23 # ipdb.set_trace() 24 25 # Print the message 26 print(response_data["headers"]["Message"]) 27 28 except requests.exceptions.RequestException as e: 29 print(f"An error occurred: {e}") 30 31 32if __name__ == "__main__": 33 hello_world()
Create a unit-test file,
helloworld_test.py, with the following contents:helloworld_test.py¶from helloworld import hello_world MOCK_RESPONSE_DATA = {"headers": {"Message": "Hello, world!"}} class MockResponse: def __init__(self, json_data): self.json_data = json_data def json(self): return self.json_data def raise_for_status(self): pass def test_hello_world(mocker, capsys): # Mock the requests.post method mocker.patch("requests.post", return_value=MockResponse(MOCK_RESPONSE_DATA)) # Call the function to be tested hello_world() # Get the printed output captured = capsys.readouterr() # Check that the output is as expected assert captured.out.strip() == "Hello, world!"
Run the unit test using
pytest:$ pytest helloworld_test.py ================= test session starts ===================== platform linux -- Python 3.12.7, pytest-8.3.2, pluggy-1.5.0 rootdir: /home/user/python plugins: cov-5.0.0, xdist-3.6.1, mock-3.14.0, requests_mock-1.12.1, typeguard-4.3.0 collected 1 item helloworld_test.py . [100%] ==================== 1 passed in 0.01s ====================