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. 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

  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.

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 with AmbiguousImplementationError. 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 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/. 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() (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: the wiring problems of everything the *_instance fields reach are reported at startup, in one error (see Known limits). 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 (also a signature that cannot be read or rebuilt, a type hint that cannot be resolved, or a Keyed marker in a form that is not supported), dependency cycles, a singleton that would capture a scoped service (also through Lazy), and project modules discovery could not import or patch. It only reads; it constructs no services. To check again later: validate_autowiring(container) (raises AutowiringValidationError with .issues). A file that could not be loaded during discovery is named on a line of its own and checked when it loads (see Files loaded after startup).

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. It must not hold Lazy[ScopedService]: a Lazy keeps the first value, so the singleton would keep the first request's object (validate=True reports it). Reading a Lazy again while it is being created raises CircularDependencyError instead of hanging.

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 with AmbiguousImplementationError when 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 (keyed and open-generic registrations included).

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")]): ...

An optional keyed dependency is written Annotated[IClock, Keyed("utc")] = None, Optional[Annotated[IClock, Keyed("utc")]] = None (or ... | None) or Annotated[Optional[IClock], Keyed("utc")] = None: the keyed service when it is registered, otherwise the default. A Keyed marker in any other place (a union with another type, inside list[...]) is not supported; it is never ignored silently - a WARNING and a validation issue name the field and the key.

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. A registration replaced after autowiring has started reaches the consumers created afterwards (no refresh() needed). Aliases of the replaced type (register_alias) follow the replacement.

Several FastAPI applications in one test session: open each with with, so its startup makes its container the active one:

def test_first():
    with TestClient(first_app) as client:
        client.get("/products")

def test_second():
    with TestClient(second_app) as client:
        client.get("/orders")

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. An unregistered class that discovery never patched (outside the project root, or created after startup) is not changed: injected() fills the None fields of the object it builds after __init__ returns, so __init__ itself cannot use them yet. Move such a class into the project (or create it after auto_inject()'s discovery) if __init__ needs its fields. A registered type whose class was never patched is not filled by injected() (a shared singleton would keep the first request's objects): each None field stays None with a WARNING naming it - move the class into the project root or use constructor injection.

Logging and debug output

The library logs to logging.getLogger("py_autowired") and writes nothing while everything is fine. Anything that could hide a problem is a WARNING (or an ERROR), never a DEBUG-only line - shown when the application configures logging (the library adds no handler of its own):

  • a project file that cannot be imported or discovered;
  • a field with no matching registration (once per field; it stays None);
  • a contract registered without an implementation that discovery did not find;
  • fields whose best candidates tie: ONE grouped WARNING at startup; the first registered candidate is used (validate=True reports them as errors). Two registrations of the same ready object are not a tie; what a factory returns cannot be known in advance, so two factories returning one object still are;
  • constructor type hints that cannot be resolved (a parameter with a default keeps its default);
  • a scoped service resolved while a singleton is being created (if the singleton keeps it, it outlives its scope) - once per pair;
  • a project module imported by a loader the import hook cannot patch (outside excluded folders such as tests);
  • auto_inject() that found nothing to load and nothing to patch (check root_dir).

Set PY_AUTOWIRED_DEBUG=1 before import for detailed DEBUG output.

Files loaded after startup

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. It is not lost; its problems are logged and checked:

  • startup logs it at INFO: Skipped main during discovery: ... it is checked when it loads;
  • the moment it loads, its *_instance fields are filled and checked the way validate=True checks them. A problem is an ERROR at once; with validate=True the import of that file fails with AutowiringValidationError. A healthy file logs INFO Checked main after it loaded ...;
  • if the application never imports it (a server started from the application module itself), the library imports it at the first safe moment: the first field injection after the application module has finished loading, or refresh(), activate_container(), validate_autowiring(). Discovery imports every project file anyway; this is the same import, done later;
  • skipped_during_discovery() returns the names, and check_skipped() imports and checks all of them, raising AutowiringValidationError for any problem whatever validate was:
from py_autowired import check_skipped

def test_files_skipped_at_startup_are_wired():
    import app            # the application starts
    check_skipped()       # raises on any problem

The root fix is in the application: the skip happens because discovery starts while the application module is still being imported. Start it after that import has finished and nothing is skipped. The bridges already do this (attach_container in FastAPI's lifespan, Flask and Django at the first request). A startup that calls auto_inject() itself can do the same; it then also gives each request its own scope and closes the container at the end (what attach_container does for you):

# app.py - calls auto_inject() itself instead of attach_container()
from contextlib import asynccontextmanager
from fastapi import FastAPI
from py_autowired import Container, auto_inject
from register import register

container = Container()
register(container)

@asynccontextmanager
async def lifespan(app):
    auto_inject(container)   # app.py has finished importing: main.py loads normally
    yield
    await container.aclose()

app = FastAPI(lifespan=lifespan)

@app.middleware("http")
async def request_scope(request, call_next):
    async with container.create_scope():   # scoped services live for one request
        return await call_next(request)

If such a file finishes loading in another thread while the library is busy, its import cannot be failed any more: the problem is an ERROR at once and, with validate=True, the next library call (refresh(), activate_container(), auto_inject(), validate_autowiring() or the next injection) raises it after doing its own work. A skipped file that has not loaded yet is named on a line of its own by validate_autowiring().

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. Each scoped service is created under its own gate, so a factory may wait for a worker thread that needs another scoped service of the same scope. Known limit: a factory that waits (for example with thread.join()) for a worker needing the very service the factory is creating waits forever - no lock shows that wait, for singletons as for scoped services. It is not silent: a thread that waits at a creation gate longer than container.creation_wait_warning seconds (10 by default) logs one WARNING naming both threads, written at once by a background writer (a log handler never blocks a waiting thread; if many warnings pile up, the oldest are dropped and their number is written). The same applies to a Lazy value (two threads reading each other's Lazy raise CircularDependencyError), to closing a scope while a creation in it is still running, and to the library's own lock (refresh(), activate_container(), ... waiting while auto_inject() imports modules). The suite runs on the free-threaded CPython 3.14t build.
  • Shutdown of the Flask/Django bridges: container-created singletons are closed at process exit; one that only has aclose() is closed in a short event loop.
  • One container per process: class patches are process-wide; several applications in one process are not supported - run them in separate processes (real support comes in 0.6.0, with a per-request container). A request (FastAPI, Flask, Django) or an injected() call whose container is not the active one fails before anything is built. This check is a best-effort catch, not a guarantee. Applications used one after another (tests) are fine: each startup makes its own container the active one.
  • 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 (as are venv, .venv, site-packages, build, dist). A vendored copy of a library, or a second installation, kept inside the project must be excluded by name: auto_inject(container, exclude_dirs=["vendor", "third_party"]).
  • What counts as a field: a name ending in _instance/_instances that can receive a value - a class attribute, an annotation, a self.x_instance = ... assignment, a __slots__/attrs/dataclass field or a property with a setter. A method or a read-only property with such a name (has_instance) is not a field.
  • 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.

Known limits (0.5.4)

What 0.5.4 does not promise, in one place:

  • Logs reach you only through your logging setup. The library adds no handler of its own: without a configured logging, its WARNINGs and ERRORs are not shown.
  • Validation covers the graph reachable from *_instance fields. A registered service no field reaches is not checked. A wrong or empty root_dir is a WARNING, also with validate=True (code that only registers services by hand is valid).
  • Many problems are made visible, not prevented. The library writes a WARNING and goes on in these cases: a factory that waits (thread.join(), an Event) for a worker needing the very service being created, and an import during auto_inject() that waits for another thread using the library, can hang (a WARNING names both threads after 10 s); equally good candidates use the first registration; a singleton that resolves a scoped service may keep it; a property whose getter fails is skipped; a class created while the library is busy gets its fields only once the library is free; a Keyed parameter with a default whose type hints cannot be resolved uses the default. "Coming in 0.6.0" lists those that will become errors.
  • Several applications in one process: NOT SUPPORTED, not solved in 0.5.4. Field injection uses one active container for the whole process. The check made when a request starts is a best-effort catch: a container activated while the request runs, and objects built OUTSIDE a request (a script, a background thread), are not caught - they use the active container. In a thread the library does not see (one you start without passing the context on), nothing in the process tells which application an object belongs to, so this cannot be solved there at all; 0.5.5 brings a router for several live containers, 0.6.0 an explicit error for objects built without any context. Without any active container (no application has started yet, e.g. only TestClients without with), fields are not filled, as in 0.5.3: each one stays None with a WARNING naming it. Mixing the two does not work: after an application opened with with has stopped, its container stays active, so every request of another application used without with fails - open every application with with, or call activate_container(container) first.
  • A closing scope hands its objects only to the code closing it (its finalizers and what they start with a copy of their context), and only those whose finalizer has not run yet; anyone else gets ScopeNotActiveError. An object you took from the scope BEFORE it closed still works on whatever it holds - using a resource that its finalizer has closed is not prevented.
  • Threads on the free-threaded 3.14 build: a new thread no longer inherits the library's context (another request's scope). A plain threading.Thread started inside a request therefore does not see the request's scope, as on 3.11; pass it on with threading.Thread(..., context=contextvars.copy_context()) or use asyncio.to_thread. This uses a sys.monitoring tool id (4, or 3 if 4 is taken); a tool that needs exactly these ids conflicts with it. If none is free, its check fails at import, or the hook fails later, the library falls back to a hard rule for the rest of the process: a scope is read only in the thread that entered it, and every other thread - also asyncio.to_thread and the anyio workers FastAPI's sync routes use - gets ScopeNotActiveError, never another request's objects. A fall-back at import logs one WARNING; a fall-back later logs nothing (the hook may run under a lock) - every refused read says why in its ScopeNotActiveError message (STATUS=hard-rule). The state is py_autowired._thread_birth.STATUS: "active", "hard-rule" or "not-needed" (threads do not inherit on this build). There is no way back without restarting the process. A second copy of the package loaded in the same process (the library's own source imported again under another name) always uses the hard rule for its own containers - unless the first copy fell back at import and the tool ids became free later: then the new copy may set up its own hook and be "active" itself (the first copy keeps the hard rule).
  • A scope closed inside a factory (a factory that opens and closes a scope of its own) runs that scope's finalizers while the factory's creation lock is held. The library's own closing never does: when the closing thread holds a lock a creation in that scope waits for, nothing is closed, the scope stays open and CircularDependencyError is raised - close it again without the lock.
  • A registered type whose class was never patched is checked for None fields by reading plain values only (its own plain dictionary, plain class values, slots) - no code of the object or its class runs. When that is not possible (the class defines its own __dict__, the object's __dict__ is a dict subclass, a dictionary has a key that is not a plain string, or a field-like name is a property or another descriptor), one WARNING per class says its fields cannot be checked.
  • A once-only WARNING whose key holds a class's __module__ or __qualname__ that is not a string (set so by user code) is written every time instead of once: the library never keeps or hashes such an object.
  • Code that runs under the library's lock: importing the project's modules during discovery, and the __hash__/__eq__ of registered types and keys, run while the library holds its lock. They must not wait for another thread that resolves from the same container (after 10 s a WARNING names both threads). The __del__ of an object the library lets go (an old name map's registrations, say) may run while a singleton is being created: do not resolve from a container or wait for another thread in __del__.
  • A field inherited from a base outside the project is read from the base's class body, __slots__, annotations and the assignments its plain methods make on self (also a self a closure captures). In its static and class methods every *_instance assignment counts, whatever the object (they may be given self): a field of another object may be reported too - a visible WARNING, or a validation issue, never a silent None. An assignment on an object the scan cannot name (a call's result) is counted - in doubt a field is reported, never lost; one through another name (alias = self; alias.x_instance = ...) or under a computed name (setattr) is not seen. Such a field - or an annotation of any base - does not make the class a "field injection only" class: its constructor dependencies are resolved as before. Such a base's own attribute with a *_instance name is filled when a registration matches it (see CHANGELOG, "Behaviour changes"). A base read once is not read again: a method added to it afterwards is not looked at. Which assignments are made on self is read from the bytecode, checked once per process on known shapes (3.10, 3.11, 3.14, 3.14t tested); on a Python where that check fails, one WARNING says so and every assignment counts (a field of another object may be reported too, none is lost). A base counts as inside the project when its file is under a root any auto_inject() call of the process used: after calls with different roots, a base under an earlier root is not read (its field is not a candidate, as before 0.5.4).
  • Speed of injected() against 0.5.3 (CPython 3.11, Windows, one call of the dependency itself, no network, warm caches): a registered singleton about +10% (~110 ns), a registered transient about +9% (~490 ns), an unregistered controller about +6% (~270 ns). What costs it, on purpose: the check that the request's container is the active one (also in each dependency, the only check for a WebSocket or a dependency outside the middleware), the check of a registered type's fields (one identity comparison once its class is known to be patched) and the shared post-init loop.
  • Per-request cost of the library against 0.5.3 (CPython 3.11, Windows: the request middleware - active-container check, scope open and close - and one injected() dependency, without the web framework): an empty scope about 17.7 µs (0.5.3: 12.6), a registered singleton about 19.6 µs (14.0), a scoped service about 49.7 µs (24.8). Why, on purpose: no log handler runs while a library lock is held (the scope's lock counts as one), the free-threaded build's thread-birth rule reads the entering thread, the closing scope marks its finalizers' context, and a scoped service is created behind its own creation gate - two concurrent requests for it in one scope never build two objects, and a lock cycle raises instead of hanging. The scoped creation path is the largest part; its redesign is planned for 0.5.5. A web request through FastAPI on the same machine adds about 150-230 µs of its own.
  • When a log record is written: while a thread holds a library lock (a creation lock, the library lock, the registration, provider, scope, name map, thread-birth or startup lock), every record of the library (any level) is kept and written when that thread releases the last one - a log handler may take locks of its own. A resolution that takes no lock (a transient) writes at once. A record of a background thread may therefore appear a moment after that thread released the lock.
  • Class metadata changed after registration (e.g. a registered class's __name__) is not seen by the name map on its own - call refresh().
  • A late import racing a change of the import system: a module imported for the first time after auto_inject() is patched only if the late-import hook's quick check and Python's own search see the same thing. They may not in two cases: the module's file is created during those microseconds of its own import, or another thread changes sys.path or sys.path_importer_cache while the import runs. Then the module loads unpatched and its *_instance fields stay None, with no message. Python itself does not treat either case as safe; avoid them by writing project files before importing them and changing sys.path before starting threads that import.
  • (Resolved in 0.5.4) Two limits of earlier changelogs no longer hold: replace() moves the aliases of the replaced type along, and fields assigned in an inherited __init__ without annotation are injected.

Coming in 0.6.0

These change behaviour that working code may rely on, so in 0.5.4 they are only visible (a WARNING or a validation issue); 0.6.0 changes them:

  • Equally good candidates become a startup error, with an explicit "first registered" choice for code that wants today's behaviour.
  • A singleton capturing a scoped service becomes an error (Provider is the way to use a scoped service from a singleton).
  • Required and optional *_instance fields are told apart explicitly.
  • Incomplete discovery (a file that cannot be imported, an unsupported loader) stops startup unless allowed.
  • One construction plan shared by the container and validation, and a startup that imports user code outside the library lock.
  • An explicit policy for properties whose getter fails.
  • A Keyed parameter with a default whose type hints cannot be resolved fails instead of using the default.
  • Several applications in one process: a per-request container (ContextVar) instead of one active container per process.

To be evaluated for 0.6.0: an optional check of every registration (not only those the fields reach) and a report that explains why a field received the service it did. Order of work: the shared construction plan and the startup outside the library lock come first.

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. The code changed in 0.5.3 and 0.5.4 was tested with hand-written mutants; the final run on the release code (each run first runs the tests without a mutant, and stops if they do not pass): 464 mutants on CPython 3.11 - 457 killed, 7 survived; 5 on 3.14 - all killed; 35 on the free-threaded 3.14t - 34 killed, 1 survived; a 169-mutant locking subset on 3.14t - 168 killed, 1 survived. The survivors were closed with tests and a targeted run killed every one of them; 165 retired mutants are listed with their reasons (5 of them equivalent). Full report: docs/MUTATION_REPORT.md.

License

MIT

Metadata

Release files for py-autowired 0.5.4

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.4
File Size Uploaded Provenance Actions
py_autowired-0.5.4.tar.gz 294.8 kB With provenance Details for py_autowired-0.5.4.tar.gz

Built distribution (wheel)

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

Total release size: 370.7 kB

Release files / py_autowired-0.5.4.tar.gz

Download URL py_autowired-0.5.4.tar.gz
Size 294.8 kB
Tags Source
SHA-256 checksum
How to use checksums
d6b3a38d3cb8cd3d2ab2481392c73ad1927b1e46a2fc989fa39a74ce24223c8c
BLAKE2b-256 checksum
How to use checksums
8df325a7468213f68899b310f8178be712ac93a8e9e6b0d104bab5d4d49e3d9d
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 24, 2026.

Transparency log

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

Download URL py_autowired-0.5.4-py3-none-any.whl
Size 75.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8724a1e6385e6779b10e7ca9d4d9c3275ecaba2cc7040fe49538abfdf59737ac
BLAKE2b-256 checksum
How to use checksums
76676bda9c7a9660167ae3898f12c20f8206bddc558dcd7c4877c17ff991f9d9
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 24, 2026.

Transparency log

Security for py-autowired 0.5.4

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

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for py_autowired-0.5.4.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 24, 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.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

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