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

  1. (Optional) Create a directory for Python development, as well as a directory for the new project:

    mkdir -p ~/python/helloworld
    cd ~/python/helloworld
    
  2. Create a separate virtual environment for the new project (specifying .venv as the directory for it):

    python3 -m venv .venv
    
  3. Activate the virtual environment by sourcing the activate script:

    source .venv/bin/activate
    
  4. 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.

  1. Create a requirements.txt file with the list of dependencies. For example:

    echo "requests" > requirements.txt
    
  2. 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.31charset-normalizer 3.4.1idna               3.10pip                24.2requests           2.32.3urllib3            2.4.0

    (The version numbers can differ based on your environment.)

  3. 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()
    
  4. 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.

  1. Add ipdb to the list of dependencies:

    echo "ipdb" >> requirements.txt
    
  2. Install the dependencies:

    pip install -r requirements.txt
    
  3. 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()
    
  4. 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 the pp (pretty print) command and then by specifying the Message 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.

  1. 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!"
    
  2. 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 ====================