Skip to main content

py_autowired

CI PyPI

Runtime dependency injection for Python, without imports between your classes.

A class only says what it needs:

class ProductService:
    def __init__(self):
        self.product_repository_instance = None   # filled automatically

py_autowired fills product_repository_instance at runtime. ProductService does not import the repository, does not receive it as a constructor argument, and does not call any container. The classes stay independent of each other. The only place that imports them is one register/ folder.

Türkçe: KULLANIM_KILAVUZU_TR.md

Install

pip install py_autowired

Quick start (FastAPI)

1. Your classes - they depend on contracts (interfaces / abstract classes) and never import each other; an implementation imports only the contract it implements:

# application/product_service.py
from domain.product_service import IProductService

class ProductService(IProductService):
    def __init__(self):
        self.product_repository_instance = None   # -> IProductRepository

    def list_products(self):
        return self.product_repository_instance.list_products()

# presentation/product_controller.py
class ProductController:
    def __init__(self):
        self.product_service_instance = None      # -> IProductService

    def list_products(self):
        return self.product_service_instance.list_products()

2. register/register.py - the only file that imports the classes; it says which class serves each need and how long it lives:

from application.product_service import ProductService
from domain.product_repository import IProductRepository
from domain.product_service import IProductService
from infrastructure.memory_product_repository import MemoryProductRepository


def register(container):
    container.register_scoped(IProductRepository, MemoryProductRepository)
    container.register_transient(IProductService, ProductService)

3. bootstrap/bootstrap.py - how the application comes up:

from py_autowired import Container
from py_autowired.fastapi import attach_container

from register import register


def bootstrap(app):
    container = Container(strict_interfaces=True)   # only contracts can be registered
    register(container)
    attach_container(app, container)

4. main.py - one call:

from fastapi import FastAPI
from bootstrap import bootstrap
from presentation.product_controller import ProductController

app = FastAPI()
bootstrap(app)

@app.get("/products")
def products():
    return ProductController().list_products()   # fields are already filled

Run it with uvicorn main:app or python main.py. There is no root_dir, no lifespan and no middleware to write: the project is scanned when FastAPI starts (in its lifespan), every request gets its own scope, and singletons are closed when FastAPI stops. The complete project is in examples/fastapi.

The three rules

  1. Declare, do not import. A field ending in _instance and set to None is a dependency. Its name selects the contract: product_service_instance means IProductService (see Naming). A value you assign yourself is never overwritten; a field with no matching registration stays None.
  2. Register in one place. register/ imports the classes and says which class to create for each need and how long it lives.
  3. Bootstrap once. bootstrap/ creates the container, adds the registrations and connects the framework with one bridge call (see Frameworks); the entry file calls bootstrap(...) once.

Type annotations are optional (product_service_instance: IProductService | None = None also works). They help your editor, but they need an import; the name alone is enough for py_autowired.

Naming: which field gets which class

The field name without _instance is compared with the registered contract and class names. Underscores and upper/lower case do not matter, and these contract prefixes (and the Impl suffix) are understood:

Contract name Field that receives it
IProductService, I_ProductService product_service_instance
ABSProductService, ABS_ProductService product_service_instance
AbsProductService, Abs_ProductService product_service_instance
absProductService, abs_ProductService product_service_instance
ProductServiceImpl product_service_instance
ProductService (no contract) product_service_instance

A prefix counts only when the next letter is upper case (or an underscore follows), so names such as Absolute or Ignore... stay ordinary words.

Registering services: container.register_*

Call Lifetime Use it for
container.register_singleton(IClock, SystemClock) one object for the whole application configuration, clocks, clients that are safe to share
container.register_scoped(IRepo, SqlRepo) one object per scope (a web request) database sessions, unit of work
container.register_transient(IProductService, ProductService) a new object every time stateless services
container.register_instance(Settings, settings) the object you give objects you created yourself; never closed by py_autowired
  • register_*(Contract, Implementation) - the clearest form.
  • register_*(Contract) - py_autowired finds the single implementation of the contract by itself, also when it lives in another layer.
  • register_*(ConcreteClass) - the class is its own implementation. Not allowed with Container(strict_interfaces=True).
  • Contracts only: Container(strict_interfaces=True) (used in every example) accepts only interfaces / abstract classes as the registered type. Then every dependency in the project points to an abstraction, never to a concrete class.
  • Keep the register_* lines in register/ - the one place that imports the application classes.
  • Register a class when another class needs it. A class that only receives dependencies (a controller) is created normally: ProductController().
  • Scoped objects with a close()/dispose() (or aclose()) method are closed when their scope ends. Container-created singletons are closed when the application stops (web bridges) or when you call container.close().

Frameworks

Every example is a complete project with the same layers and its own README.

Framework Bridge call (in bootstrap/) Scope Example
Console auto_inject(container) with container.create_scope(): examples/console
FastAPI attach_container(app, container) every HTTP request examples/fastapi
Flask attach_container(app, container) every HTTP request examples/flask
Django application = attach_container(application, container) every HTTP request examples/django

Import the bridge from py_autowired.fastapi, py_autowired.flask or py_autowired.django. Each bridge finds the project folder by itself (the folder with requirements.txt, pyproject.toml, .git, main.py/app.py, or manage.py for Django), scans it once, opens a scope for every request, and closes singletons when the application stops.

Optional features

None of these is needed for the setup above. Each one solves a specific problem; use it only when you have that problem.

Validate at startup

Why: a missing registration normally shows up only when the field is used. Benefit: all wiring problems are reported at startup, in one error. How:

attach_container(app, container, validate=True)   # any bridge
auto_inject(container, validate=True)             # console

It lists fields with no matching registration, equally good candidates, constructor dependencies that cannot be resolved, dependency cycles, and a singleton that would capture a scoped service. It only reads; it constructs no services. To check again later: validate_autowiring(container) (raises AutowiringValidationError with .issues).

Provider[T] and Lazy[T]

Why: sometimes an object must not be created up front - it is expensive, or it belongs to a request while the holder lives longer. Benefit: create it later, at the moment it is needed. How (constructor parameters):

from py_autowired import Lazy, Provider

class ReportService:
    def __init__(self, sessions: Provider[Session], exporter: Lazy[PdfExporter]):
        self.sessions = sessions    # self.sessions() -> the Session of the current scope
        self.exporter = exporter    # self.exporter.value -> created on first use, then reused

A singleton may safely hold Provider[ScopedService]: every call resolves from the scope active at that moment.

Choosing between several implementations: set_primary

Why: two registered classes both match a field name. Benefit: decide the winner once instead of renaming fields. How: container.set_primary(UtcClock) (like Spring's @Primary). Without it, the class closest to the consumer's module wins.

Keyed services

Why: one interface, several implementations used side by side (for example a UTC and a local clock). Benefit: pick the implementation by a key. How:

from typing import Annotated
from py_autowired import Keyed

container.register_keyed(IClock, "utc", UtcClock)
container.register_keyed(IClock, "local", LocalClock)

class Invoice:
    def __init__(self, clock: Annotated[IClock, Keyed("utc")]): ...

Keyed registrations are used through constructors and resolve, not through *_instance fields.

Open generics

Why: many types need the same kind of service (Repository[User], Repository[Order], ...). Benefit: one registration serves all of them. How:

container.register_open_generic(Repository, SqlRepository)   # class SqlRepository(Repository[T])

Closed generics are used through constructors and resolve, not through *_instance fields.

Closing the container yourself

Why: a console program or script with no bridge should release resources. Benefit: singletons are disposed in reverse creation order. How: container.close() or await container.aclose() when the work is done. The framework bridges already do this when the application stops. Objects given to register_instance are never closed. After close(), resolve() raises ContainerClosedError.

Swapping a service in tests

Why: a test should use a fake instead of the real database. Benefit: the code under test stays unchanged. How:

container.replace(IProductRepository, FakeProductRepository)

Use a fresh container per test. If you replace a registration after autowiring has started, call refresh() before creating new consumers.

FastAPI parameter style

Why: you prefer FastAPI's Depends for some endpoints. How: controller=Depends(injected(ProductController)), with injected from py_autowired.fastapi. The fields of the object are filled the same way.

Logging and debug output

The library logs to logging.getLogger("py_autowired") and is silent by default. Discovery problems are logged as WARNING. Set PY_AUTOWIRED_DEBUG=1 before import for detailed DEBUG output.

Notes

  • Concurrency: ready singletons and cached lookups are lock-free. A dependency cycle raises CircularDependencyError instead of freezing, even when two threads hit it at the same time. The suite runs on the free-threaded CPython 3.14t build.
  • One container per process: class patches are process-wide; run separate applications in separate processes.
  • Discovery imports modules under the project folder. Use exclude_dirs for folders that must not be imported. tests and examples folders inside the project are skipped automatically.
  • resolve() is synchronous: a factory that returns a bare coroutine raises AsyncFactoryError.
  • Compatibility: ordinary classes, __slots__, mutable dataclasses and mutable Pydantic v2 models (with arbitrary_types_allowed=True) work. Frozen dataclasses and frozen Pydantic models cannot receive fields (an AutoInjectionResolutionError explains why).
  • Speed: an injected field costs a few microseconds per object creation on warm caches; measure on your machine with python -B benchmarks/field_injection.py --number 100000 --repeat 5.

Test quality

The suite runs on CPython 3.10, 3.11, 3.14 and the free-threaded 3.14t build (GIL disabled). The injection engine and the container were mutation tested with cosmic-ray: 1399 mutants, 99.3% killed among mutants that can change behavior (88.0% of all mutants; the rest are proven equivalent, each with its reason). The 9 mutants that are neither killed nor proven equivalent are listed openly. The code added in 0.5.1 (bridges, late imports, prefixes) was tested the same way: 329 mutants, 99.0% killed among those that can change behavior. Full report: docs/MUTATION_REPORT.md.

License

MIT

Metadata

Release files for py-autowired 0.5.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for py-autowired 0.5.1
File Size Uploaded Provenance
py_autowired-0.5.1.tar.gz 116.0 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for py-autowired 0.5.1
File Interpreter ABI Platform
py_autowired-0.5.1-py3-none-any.whl Python 3 none any Details

Total release size: 153.8 kB

Release files / py_autowired-0.5.1.tar.gz

Download URL py_autowired-0.5.1.tar.gz
Size 116.0 kB
Tags Source
SHA-256 checksum
How to use checksums
5ab435e8054e59a4621bbe4b17ab3b584d5fff577ece18c09f0630535ba875b8
BLAKE2b-256 checksum
How to use checksums
a1e1ec02300a5b978eae8ee7424a35755ffebc05be44f61d9da5fa12db458f77
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

Release files / py_autowired-0.5.1-py3-none-any.whl

Download URL py_autowired-0.5.1-py3-none-any.whl
Size 37.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3119c3d6cfaa20f063f5d36347af08a9ae583ad30fdbbeed572b216fbc67c4c7
BLAKE2b-256 checksum
How to use checksums
e6266e247f635481207a3c123176c57cd5832cc2a611392ede27c1706caa85ad
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

Security for py-autowired 0.5.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for py_autowired-0.5.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page