Drone CI’s approach to building and testing Python projects is surprisingly effective because it treats your build environment as a disposable, ephemeral container, forcing you to explicitly define every dependency and step.

Let’s see it in action. Imagine a simple Python project with a setup.py and a requirements.txt:

my_python_project/
├── setup.py
├── requirements.txt
├── src/
│   └── my_package/
│       └── __init__.py
└── tests/
    └── test_example.py

Here’s a .drone.yml that builds, tests, and even publishes this project:

kind: pipeline
type: docker
name: default

steps:
- name: build
  image: python:3.9-slim
  commands:
    - pip install --upgrade pip setuptools wheel
    - pip wheel --no-deps --wheel-dir dist .
    - echo "Build complete."

- name: test
  image: python:3.9-slim
  commands:
    - pip install -r requirements.txt
    - pip install .
    - pytest tests/
  environment:
    MY_API_KEY:
      from_secret: api_key

- name: publish
  image: plugins/docker
  settings:
    repo: yourusername/my-python-project
    tags: latest,${DRONE_COMMIT_SHA}
  when:
    branch: main
    event: push
  environment:
    DOCKER_USERNAME:
      from_secret: docker_username
    DOCKER_PASSWORD:
      from_secret: docker_password

This pipeline starts with a build step. It uses a standard python:3.9-slim Docker image. The commands first ensure pip and its friends are up-to-date, then build a wheel file for the project without installing any dependencies (pip wheel --no-deps --wheel-dir dist .). This creates a dist/ directory containing a .whl file, which is a pre-packaged version of your library.

The test step also uses python:3.9-slim. It installs the project’s runtime dependencies from requirements.txt, then installs the project itself using the wheel built in the previous step (pip install .). Finally, it runs pytest against the tests in the tests/ directory. Notice how sensitive configuration like an API key is injected via environment variables sourced from Drone secrets, ensuring they never appear in your code or pipeline definition.

The publish step, which only runs on pushes to the main branch, uses the plugins/docker image. It’s configured to push a Docker image to yourusername/my-python-project, tagging it with latest and the Git commit SHA. This step requires Docker Hub credentials, again sourced from Drone secrets.

The mental model Drone CI forces is that of a clean room. Each step starts with a fresh container. If a dependency isn’t installed in a previous step and then explicitly installed in the current one, it won’t be there. This eliminates "it works on my machine" problems because the build environment is the machine, and it’s always identical. You define your environment and its state through explicit commands in your steps.

The key to understanding Drone’s Python integration is realizing that each step is an independent Docker container execution. This means you can use any Docker image that suits your needs. For Python, you’ll most commonly use official Python images (python:3.x, python:3.x-slim, python:3.x-alpine) or images pre-configured with build tools if necessary. The commands array within a step is simply a series of shell commands executed sequentially within that container. This allows for complex build logic by chaining standard Python tools like pip, setuptools, wheel, and testing frameworks like pytest or unittest.

What’s often overlooked is how easily you can layer build stages. For example, if you needed to run type checking with mypy and linting with flake8, you’d simply add more steps, each with its own image and commands, ensuring the necessary tools are installed within that step’s container. This modularity makes complex build and test matrices manageable.

The next logical step is to integrate with code quality analysis tools or package index hosting beyond Docker Hub.

Want structured learning?

Take the full Drone course →