mrkeyoor.com_
Sat 08 Aug 17:39 UTC
PyPITestingupdated 08 Aug 2026

testcontainers

Testcontainers for Python starts disposable Docker containers from tests and tears them down afterward. It includes ready-made classes for databases, queues, browsers, cloud-service emulators, and other infrastructure, plus a DockerContainer base for arbitrary images. The payoff is testing against the real service and protocol instead of an in-memory substitute. It is test orchestration, not a mocking library, and Docker availability is part of every test run.

Verdict

Use it when fidelity to a real dependency is worth Docker startup cost and CI plumbing. Do not make every test a container test; keep the unit suite cheap and reserve this layer for behavior that fakes routinely miss.

API stability3/5The context-manager model and core container methods are established, but 4.x continues to reorganize modules and expand structured wait strategies. Version 4.15.0 publishes compatibility modules such as testcontainers.postgres while current code and docs increasingly use testcontainers.community.postgres, so upgrades deserve import and fixture testing rather than blind version bumps.
Docs4/5The documentation covers daemon discovery order, rootless sockets, connection modes, Ryuk, wait strategies, networking, file transfer, command execution, Compose, and many service modules with runnable examples. Some pages mix older convenience helpers with newer structured strategy APIs, and several examples use latest image tags, so the wheel's current signatures remain worth checking.
Maintenance5/5PyPI shows version 4.15.0 released in July 2026, the repository was pushed later that month, and it is not archived. The repository has 174 open issues and pull requests, which is substantial but unsurprising across Docker environments and dozens of community modules; active core and community test workflows show ongoing attention rather than abandonment.
Ecosystem5/5The package's fetched weekly figure is 7,561,559 downloads, and the repository has 2,268 stars. PyPI metadata lists extras for a wide range of databases, brokers, cloud emulators, search engines, browsers, and other services, while the generic DockerContainer remains an escape hatch for any image not covered by a module.

Use it if

  • Your integration tests need the behavior of a real PostgreSQL, Redis, Kafka, LocalStack, or another Dockerized service
  • You want isolated random host ports so developers and parallel CI jobs do not fight over fixed infrastructure
  • You need service-specific connection URLs and readiness checks instead of maintaining shell scripts around docker run
  • Your team accepts slower integration tests in exchange for finding migration, driver, protocol, and configuration bugs
Skip it if

Setup reality

`pip install testcontainers` installs the Python wrapper and its Docker SDK dependencies, but it does not install or start Docker. Python 4.15.0 requires Python 3.10 or newer, and the process must be able to find and access a Docker-compatible daemon. Local Docker Desktop often works immediately; CI, rootless Docker, remote daemons, and Docker-in-Docker are where setup expands. Host discovery considers `TC_HOST`, `TESTCONTAINERS_HOST_OVERRIDE`, `DOCKER_HOST`, Docker contexts, default sockets, and rootless socket locations. Containers such as Ryuk and Docker Compose may also need `TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE` because the socket path visible inside a container can differ from the client path. Ryuk starts by default as a cleanup sidecar; hardened runners may block it, while disabling it with `TESTCONTAINERS_RYUK_DISABLED` transfers orphan cleanup to you. Private images need Docker credentials, and first runs pay image-pull time. Service modules often have optional client dependencies, so install the appropriate extra or database driver, not just the base package. Postgres URLs default to the psycopg2 driver name, for example, and will fail if that driver is absent. Always pin image tags, use context managers or fixtures with finalizers, expose random host ports, and add a service-specific wait strategy. A running container is not necessarily a ready service. In parallel CI, fixed ports, shared networks, rate-limited registries, disk pressure, and leaked containers are the common surprises.

Patterns

Run a PostgreSQL integration testtest-postgres

import sqlalchemy
from testcontainers.postgres import PostgresContainer

with PostgresContainer('postgres:16') as postgres:
    engine = sqlalchemy.create_engine(postgres.get_connection_url())
    with engine.begin() as connection:
        value = connection.scalar(sqlalchemy.text('select 1'))
    assert value == 1

Pin the image tag. The generated URL defaults to a psycopg2 dialect, so install that driver or select another supported driver.

Start an arbitrary image on a random portrun-generic-container

from testcontainers.core.container import DockerContainer

with DockerContainer('nginx:1.27-alpine').with_exposed_ports(80) as nginx:
    host = nginx.get_container_host_ip()
    port = nginx.get_exposed_port(80)
    url = f'http://{host}:{port}'

Use the mapped host port returned after startup; hardcoding 80 or a fixed host binding breaks parallel runs.

Pass environment variables and a commandset-container-environment

from testcontainers.core.container import DockerContainer

container = (
    DockerContainer('redis:7-alpine')
    .with_env('ALLOW_EMPTY_PASSWORD', 'yes')
    .with_command(['redis-server', '--save', ''])
)

List commands avoid shell-tokenization surprises; secrets passed as environment variables remain inspectable in Docker metadata.

Wait for a readiness logwait-for-log

from testcontainers.core.container import DockerContainer
from testcontainers.core.wait_strategies import LogMessageWaitStrategy

container = (
    DockerContainer('my-api:test')
    .waiting_for(LogMessageWaitStrategy(r'Listening on .*:8080'))
)

The message is treated as a regular expression. Match a stable readiness line, not a log that appears before dependencies are ready.

Wait for an HTTP health endpointwait-for-http

from testcontainers.core.container import DockerContainer
from testcontainers.core.wait_strategies import HttpWaitStrategy

container = (
    DockerContainer('my-api:test')
    .with_exposed_ports(8080)
    .waiting_for(HttpWaitStrategy(8080, '/health'))
)

The default accepted status is 200; use a startup timeout appropriate to cold CI image pulls and service initialization.

Share a container with a pytest fixturepytest-fixture

import pytest
from testcontainers.postgres import PostgresContainer

@pytest.fixture(scope='session')
def postgres_url():
    with PostgresContainer('postgres:16') as postgres:
        yield postgres.get_connection_url()

Session scope saves startup time but shares database state; reset schemas or use function scope when test isolation matters more.

Execute a command inside the containerexecute-command

from testcontainers.core.container import DockerContainer

with DockerContainer('alpine:3.21') as container:
    result = container.exec(['sh', '-c', 'printf ready'])
    assert result.exit_code == 0
    assert result.output == b'ready'

A list is passed as argv; a plain string is tokenized and is not automatically interpreted as a shell pipeline.

Capture stdout and stderr after failureinspect-logs

with container:
    try:
        run_assertions(container)
    except Exception:
        stdout, stderr = container.get_logs()
        print(stdout.decode(errors='replace'))
        print(stderr.decode(errors='replace'))
        raise

get_logs returns two byte strings. Capture them before the context manager removes the container.

Mount a host configuration filemount-file

from pathlib import Path
from testcontainers.core.container import DockerContainer

container = DockerContainer('nginx:1.27-alpine').with_volume_mapping(
    Path('tests/nginx.conf').resolve(),
    '/etc/nginx/nginx.conf',
    mode='ro',
)

The Docker daemon must be able to see the host path; remote daemons and Docker-in-Docker often require a different transfer strategy.

Connect containers by network aliasnetwork-containers

from testcontainers.core.container import DockerContainer
from testcontainers.core.network import Network

with Network() as network:
    db = DockerContainer('redis:7-alpine').with_network(network).with_network_aliases('cache')
    app = DockerContainer('my-app:test').with_network(network).with_env('REDIS_URL', 'redis://cache:6379')
    with db, app:
        run_checks(app)

Use the container port and alias for container-to-container traffic; mapped host ports are for the test process on the host.

Point CI at a remote Docker daemonconfigure-docker-host

# CI environment
DOCKER_HOST=tcp://docker:2376
DOCKER_TLS_VERIFY=1
DOCKER_CERT_PATH=/run/docker-certs
TESTCONTAINERS_CONNECTION_MODE=docker_host

Remote TLS setup also needs valid client certificates, and Ryuk may require a separate socket override depending on the runner topology.

Disable the cleanup sidecar when policy blocks itdisable-ryuk

# Only when the runner supplies its own reliable cleanup
TESTCONTAINERS_RYUK_DISABLED=true

Disabling Ryuk can leave containers, networks, and volumes behind after crashes; replace it with runner-level cleanup rather than ignoring leaks.

Alternatives

PackageRegistryPick it when
pytest-dockerPyPIYou prefer pytest fixtures around a checked-in Docker Compose file and need less service-specific Python API
dockerPyPIYou want direct Docker SDK control and are willing to implement readiness, lifecycle, and cleanup yourself
pytest-testcontainersPyPIYou want a smaller pytest-oriented container fixture package for simpler test setups