Self-hosted service
gabrieldemarmiesse/python-on-whales avatar
gabrieldemarmiesse/python-on-whales

python-on-whales: a Docker CLI that reads like Python

An awesome Python wrapper for an awesome Docker CLI!

712 stars130 forksPythonMIT

At a glance

What is it?
python-on-whales is a Python wrapper for the Docker and Podman command line, exposing every subcommand as a typed Python call and returning parsed objects instead of text. It suits teams who want the CLI's coverage with a real API, and it does nothing for you when there is no Docker or Podman binary on the machine.
Who is it for?
python-on-whales is worth adopting for a Python project that has outgrown shelling out to docker and parsing its output, because it keeps the CLI as the underlying engine, so anything the CLI can do is reachable, and it returns typed objects rather than text you have to parse. It is the wrong choice for a team that wants to talk to the Docker socket directly, since the wrapper still shells out and needs the binary installed.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 8 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The mapping is one to one, and that is the whole idea

python-on-whales describes itself as an awesome Python wrapper for an awesome Docker CLI, and as a Docker client for Python designed to be fun and intuitive. The design decision underneath is that the wrapper does not reimplement Docker. It drives the command line and parses what comes back.

That is visible in the examples, which pair a shell command with the Python call that replaces it. docker run hello-world becomes docker.run("hello-world"). docker pull ubuntu becomes docker.pull("ubuntu"). docker build ./ becomes docker.build("./"). docker compose up my_service becomes docker.compose.up(["my_service"]). docker image ls becomes docker.image.list(), docker ps becomes docker.ps(), and docker cp becomes docker.copy(). The library's own summary of this is that it is the same as the CLI we all know and love.

The consequence is a large surface with no need to enumerate it. Because the wrapper is a thin layer over a command that already has every subcommand, the API grows as Docker grows, and the maintainer's job is to keep the mapping and the parsing in step rather than to write an SDK for the Docker Engine API.

The entry point is one import:

python
from python_on_whales import docker

and the object it returns is what you call methods on. The docs site is gabrieldemarmiesse.github.io/python-on-whales, and the project works on Linux, macOS and Windows for Python 3.8 and above.

Return values are objects, which is the actual upgrade over shelling out

The difference from running the command yourself shows up in the return values. The readme's example of a pull shows the progression from raw output to structured data:

python
>>> from python_on_whales import docker
>>> my_docker_image = docker.pull("ubuntu:20.04")
20.04: Pulling from library/ubuntu
e6ca3592b144: Downloading [=============>                                     ]  7.965MB/28.56MB
534a5505201d: Download complete
990916bd23bb: Download complete

>>> print(my_docker_image.repo_tags)
['ubuntu:20.04']

>>> docker.image.list()
[python_on_whales.Image(id='sha256:1a437e363abfa', tags=['ubuntu:20.04'])]

>>> my_docker_image.remove()

So a pull returns an object with a repo_tags attribute, the list command returns a list of Image objects with id and tags, and the removal is a method on the object rather than a separate command. The type name in that repr, python_on_whales.Image, is a hint at the model layer underneath, and the dependency list confirms it: the runtime requirements are pydantic and typing_extensions, and the pydantic constraint is pydantic>=2,<3,!=2.0.*, so a recent pydantic 2.

Running a container returns its output as text, with the process output captured rather than streamed to your terminal:

python
>>> from python_on_whales import docker

>>> output = docker.run("hello-world")
>>> print(output)

Hello from Docker!
This message shows that your installation appears to be working correctly.

...

And a command with arguments and an option is a normal call with keyword arguments, as in the GPU example, where gpus="all" is passed through to the CLI:

python
>>> from python_on_whales import docker
>>> print(docker.run("nvidia/cuda:11.0-base", ["nvidia-smi"], gpus="all"))

The build example shows the same, with tags="some_name" and a note that it uses Buildx or buildkit by default, and the progress output of the underlying build appearing in the terminal rather than being returned.

Two runtime dependencies, and a deliberate Python 3.8 floor

The dependency picture is unusually light for a library of this size, and that is a design statement rather than an accident of packaging.

The runtime requirements are pydantic, typing_extensions, and nothing else. The pyproject.toml marks the dependencies as dynamic and reads them from requirements.txt, which contains exactly pydantic>=2,<3,!=2.0.* and typing_extensions. There is no requests, no docker-py, no websocket client. The reason is that the wrapper does not speak the Docker Engine API, it runs the command line, so it does not need an HTTP client or a socket library.

The Python floor is 3.8, expressed as requires-python = ">=3.8, <4", and the ruff configuration sets target-version = "py38" to match, which is the consistent thing to do. The upper bound excludes Python 4.

The build system is setuptools with the setuptools.build_meta backend, and the pyproject.toml carries a comment explaining a temporary workaround for two linked issues, one in astral-sh/uv and one in pypa/setuptools, and the license-files setting is emptied as part of it. There is a MANIFEST.in at the top level and a requirements.txt that the build reads.

The console script is declared in project.scripts as python-on-whales pointing at python_on_whales.command_line_entrypoint:main, so the package installs a command line entry point of its own as well as the library.

Docker and Podman are both first class, and the tests are marked by engine

The project is not Docker-only, and the test configuration is where that shows. The pytest configuration in pyproject.toml defines two custom markers, docker for tests that need a Docker engine to run and podman for tests that need a podman engine to run, and it sets xfail_strict = true along with pythonpath = ["./"].

That marker scheme is the clearest statement of the project's stance on the second engine. Rather than pretending the wrapper talks to one thing, the test suite is split by which binary is present, and the install documentation is shared: the README sends you to the same page, the docker_client one, for instructions on installing Docker or Podman.

The development tooling is worth a mention too, because it explains the shape of the code. The dependency group is large and documentation-heavy: mkdocs with the material theme, mkdocstrings and mkdocstrings-python for pulling docstrings into the reference, griffe for the static analysis that mkdocstrings uses, astunparse for parsing Python ASTs, and markdown with pymdown-extensions for the guide pages. Ruff and ruff's isort rules handle the linting, pytest and pytest-mock handle the tests, and a pre-commit configuration is present along with .flake8 and .isort.cfg.

The top-level tree also shows a .python-version file, a uv.lock, a scripts/ directory and a docs/ directory. A uv.lock alongside a setuptools build is a sign of a modernised development environment layered on top of a package that still publishes through the older path.

Where python-on-whales is the wrong tool

The first limit is the binary dependency. The wrapper drives the docker or podman command line, so the command has to exist on the machine, and it has to be a version whose output the wrapper knows how to parse. That is a real operational constraint in a container image, in a CI runner where you wanted to avoid installing Docker, or on a host where only a remote daemon is reachable. If your deployment model is a managed remote Docker API with no local CLI, this library does not fit and you want the Engine API client instead.

The second is the cost of parsing text. Reading a command's stdout is a heuristic operation, and the parser has to make choices about what a line means. A CLI that changes its output format between versions, or adds colour codes or progress output, can break the parsing. The wrapper tracks the CLI closely enough to handle that, but the coupling is real and it is a coupling to the CLI rather than to a documented API contract.

The third is that the API is only as good as the CLI's. Some operations have no useful structured output to parse, and some flags exist only as raw strings. Where the underlying command is awkward, the wrapper is awkward in the same way, and the documentation itself is organised by CLI subcommand rather than by task, which tells you where the mental model comes from.

The fourth is Python 3.8 as the floor. That is generous today, but it is a floor, and the project excludes Python 4, so an unusual environment can be excluded.

The fifth is the shape of the project as a research-friendly tool rather than a framework. It is MIT licensed, and the release cadence is steady rather than fast, with v0.79.0 in October 2025, v0.80.0 in January 2026 and v0.81.0 in March 2026, and the last push on 2026-10-15. The version is still below 1.0, so the API can still move.

Editorial conclusion

python-on-whales is worth adopting for a Python project that has outgrown shelling out to docker and parsing its output, because it keeps the CLI as the underlying engine, so anything the CLI can do is reachable, and it returns typed objects rather than text you have to parse. It is the wrong choice for a team that wants to talk to the Docker socket directly, since the wrapper still shells out and needs the binary installed. Before you adopt it, check the three things that decide the outcome: that the Docker or Podman command line is present on the machines that run your code, because the wrapper drives the binary rather than replacing it; that the Python version floor, 3.8, and the dependency set, which is only pydantic and typing_extensions, match your environment; and that the CLI's behaviour is the behaviour you get, since a wrapper that parses the CLI's output inherits the CLI's flag names, its error text and its version-specific quirks, and the docs point at the Docker and Podman installation pages rather than restating them.

Frequently asked questions

How do I install python-on-whales and call the Docker CLI from Python?

Install it with pip install python-on-whales, then import the docker object with from python_on_whales import docker. The call mirrors the CLI: docker.run("hello-world") for docker run, docker.pull("ubuntu") for docker pull, docker.build("./") for docker build, and docker.compose.up(["my_service"]) for docker compose up. It needs the Docker or Podman command line installed on the machine.

What does python-on-whales return instead of command output?

It returns typed objects. A pull returns an object with a repo_tags attribute, docker.image.list() returns a list of Image objects with id and tags, and container removal is a method on the returned object. The runtime dependencies are pydantic, for the models, and typing_extensions.

Does python-on-whales work with Podman as well as Docker?

Yes. The test suite defines separate pytest markers, docker for tests needing a Docker engine and podman for tests needing a podman engine, and the install instructions cover installing Docker or Podman on the same documentation page. The library needs the command line of whichever engine you use to be present on the machine.

Official sources

  1. gabrieldemarmiesse/python-on-whales on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/gabrieldemarmiesse-python-on-whales.svg)](https://hysenlabs.com/projects/gabrieldemarmiesse-python-on-whales)

Was this useful?