py_autowired
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.
One register/ folder lists the contracts they need; py_autowired finds the
class behind each contract by itself.
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 - lists the contracts and how long each object
lives. It names no implementation class: py_autowired finds the single class
that implements each contract:
from domain.product_repository import IProductRepository
from domain.product_service import IProductService
def register(container):
container.register_scoped(IProductRepository) # finds MemoryProductRepository
container.register_transient(IProductService) # finds 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
- Declare, do not import. A field ending in
_instanceand set toNoneis a dependency. Its name selects the contract:product_service_instancemeansIProductService(see Naming). A value you assign yourself is never overwritten; a field with no matching registration staysNone. - Register in one place.
register/imports the classes and says which class to create for each need and how long it lives. - Bootstrap once.
bootstrap/creates the container, adds the registrations and connects the framework with one bridge call (see Frameworks); the entry file callsbootstrap(...)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.
How a field may be spelled. Underscores and upper/lower case in the field
name do not matter either. These four fields all receive SqlProductRepository:
self.sql_product_repository_instance = None # Python style (recommended)
self.sqlproductrepository_instance = None
self.sqlProductRepository_instance = None # Java / C# style
self.SqlProductRepository_instance = None
Only the ending is fixed: _instance receives one object, _instances receives
every implementation of a contract as a list (see Several implementations of
one contract).
Registering services: container.register_*
| Call | Lifetime | Use it for |
|---|---|---|
container.register_singleton(IClock) |
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) |
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)- the form used in every example: py_autowired finds the single implementation of the contract by itself, also when it lives in another layer.register_*(Contract, Implementation)- needed when a contract has more than one implementation. Without it py_autowired does not guess; it stops withAmbiguousImplementationError. Register the contract once per class to use several of them (see Several implementations of one contract).register_*(ConcreteClass)- the class is its own implementation. Not allowed withContainer(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 inregister/. Then no file imports an implementation class: imports point only to the contracts. - 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()(oraclose()) method are closed when their scope ends. Container-created singletons are closed when the application stops (web bridges) or when you callcontainer.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.
Several implementations of one contract
Why: one contract, several classes used side by side - for example a
memory and a SQL repository, or several notification channels.
Benefit: as in .NET (IEnumerable<T>) and Spring (List<T>), every
implementation is available - still through *_instance fields, with no
imports and no constructor parameters.
How: register the contract once for each class:
# register/register.py
container.register_scoped(IProductRepository, MemoryProductRepository)
container.register_scoped(IProductRepository, SqlProductRepository)
# application/report_service.py - imports nothing
class ReportService:
def __init__(self):
self.memory_product_repository_instance = None # -> MemoryProductRepository
self.sql_product_repository_instance = None # -> SqlProductRepository
self.product_repository_instance = None # -> SqlProductRepository (the last one)
self.product_repository_instances = None # -> [MemoryProductRepository, SqlProductRepository]
def count(self):
return sum(len(repo.list_products()) for repo in self.product_repository_instances)
| Field | Receives |
|---|---|
named after a class (sql_product_repository_instance) |
that class |
named after the contract (product_repository_instance) |
the last registered class - as before, and as resolve() |
contract name + _instances (product_repository_instances) |
all of them as a list, in registration order |
- Each registration keeps its own lifetime (singleton, scoped or transient).
- Registering the same class again does not add a second copy;
replace()removes all earlier registrations of the contract. - In code without fields:
container.resolve_all(IProductRepository)returns the same list. register_scoped(IProductRepository)alone, without a class, still stops withAmbiguousImplementationErrorwhen two implementations exist: py_autowired does not guess which one you mean.Container(strict_registrations=True)forbids registering a contract twice, so this feature is not available there.
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. With fields, use Several implementations of one contract:
there the class name works as the key.
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.
A project file that imports the application module while that module is
still starting (for example main.py doing from app import app) cannot be
loaded during discovery. This is not an error: it is logged at DEBUG only,
and its *_instance fields are still filled when the file is imported later.
Notes
- Concurrency: ready singletons and cached lookups are lock-free. A
dependency cycle raises
CircularDependencyErrorinstead 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_dirsfor folders that must not be imported.testsandexamplesfolders inside the project are skipped automatically. resolve()is synchronous: a factory that returns a bare coroutine raisesAsyncFactoryError.- Compatibility: ordinary classes,
__slots__, mutable dataclasses and mutable Pydantic v2 models (witharbitrary_types_allowed=True) work. Frozen dataclasses and frozen Pydantic models cannot receive fields (anAutoInjectionResolutionErrorexplains 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; the code changed in 0.5.2 (several implementations per contract, upper case field names): 216 mutants, 100% of those that can change behavior killed. Full report: docs/MUTATION_REPORT.md.
License
MIT
Metadata
Release files for py-autowired 0.5.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| py_autowired-0.5.3.tar.gz | 130.6 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| py_autowired-0.5.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 171.7 kB
Release files / py_autowired-0.5.3.tar.gz
| Download URL | py_autowired-0.5.3.tar.gz |
|---|---|
| Size | 130.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
58a262048d050b2be73596f71107e8ecaa21f88126bca69ceb81b452e75afb26
|
|
BLAKE2b-256 checksum How to use checksums |
366e0307d3084bab87979fa8e537ed8a19cb6a30b6c043968445fd7ffba0d8e9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 21, 2026.
Transparency logRelease files / py_autowired-0.5.3-py3-none-any.whl
| Download URL | py_autowired-0.5.3-py3-none-any.whl |
|---|---|
| Size | 41.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cf931aa3725af9f17d0cc77d70656a7239a9571e373b3038a12d19e87c8aa0b8
|
|
BLAKE2b-256 checksum How to use checksums |
28d476fb8fb81a62b18d0e9c0b55e4dfddf9d82971bad92f9a5f6f3c8485f4d0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 21, 2026.
Transparency logSecurity for py-autowired 0.5.3
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.
Attestation for py_autowired-0.5.3.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 21, 2026.
Transparency logA 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.