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
.venv
as the directory for it):python3 -m venv .venv
Activate the virtual environment by sourcing the
activate
script: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.txt
file 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 list
Package 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.py
file 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
POST
method printed to standard output:dev@ubuntu:~/python/helloworld$
python3 helloworld.py
Hello, 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:
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 1
Reformatting 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
ipdb
to the list of dependencies:echo "ipdb" >> requirements.txt
Install the dependencies:
pip install -r requirements.txt
Add
ipdb
module 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 31if __name__ == "__main__": 32 hello_world()
Execute the script to interact with the
ipdb
debugger:$ python3 helloworld.py > /home/rkratky/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_data
variable using thepp
(pretty print) command and then by specifying theMessage
header 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.
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 ====================