A lightweight Python framework for managing the lifecycle and dependencies of stateful components
Project description
Python Components
A lightweight Python framework for managing the lifecycle and dependencies of stateful components.
Inspired by Stuart Sierra's Component library for Clojure.
Overview
Component is a small framework for managing the lifecycle of software components that have runtime state. It provides:
- Dependency injection using explicit dependency declarations
- Lifecycle management with
startandshutdownmethods - Automatic dependency resolution using topological sorting
- Clean separation between component definition and composition
Installation
uv add python-components
Or with pip:
pip install python-components
Quick Start
1. Define Your Components
from python_components import Component
class Database(Component):
def __init__(self, host: str, port: int):
super().__init__()
self.host = host
self.port = port
self.connection = None
def start(self):
"""Connect to the database."""
self.connection = connect_to_db(self.host, self.port)
print(f"Database connected to {self.host}:{self.port}")
def shutdown(self):
"""Close the database connection."""
if self.connection:
self.connection.close()
print("Database connection closed")
class Cache(Component):
def __init__(self):
super().__init__()
self.database = None # injected by the System, see below
self.data = {}
def start(self):
"""Warm the cache using the injected database dependency."""
self.data = self.database.load_initial_cache()
print("Cache warmed from database")
def shutdown(self):
"""Clear the cache."""
self.data.clear()
print("Cache cleared")
class WebServer(Component):
def __init__(self, port: int):
super().__init__()
self.port = port
self.server = None
def start(self):
"""Start the web server."""
self.server = start_server(self.port)
print(f"Web server started on port {self.port}")
def shutdown(self):
"""Stop the web server."""
if self.server:
self.server.stop()
print("Web server stopped")
2. Compose Your System
from python_components import System
# Build the system
system_map = {
"database": Database(host="localhost", port=5432),
"cache": Cache().using(["database"]),
"web_server": WebServer(port=8080).using(["database", "cache"]),
}
system = System(system_map)
3. Start and Stop the System
# Start all components in dependency order
system.start()
# Your application runs...
# Shutdown all components in reverse dependency order
system.shutdown()
Key Concepts
Components
A component is any object that:
- Extends the
Componentabstract base class - Implements
start()andshutdown()methods - May depend on other components
Dependencies
Declare dependencies using the using() method:
component = MyComponent().using(["dependency1", "dependency2"])
Before the component's start() runs, the System sets each declared dependency as an
instance attribute on the component, named after the key in the system map:
cache = Cache().using(["database"])
# before cache.start() is called, System does the equivalent of:
# cache.database = system_map["database"]
If you want the injected attribute to have a different name than the system-map key (for example, to avoid a name clash or to keep the component decoupled from a specific system-map layout), pass a mapping of attribute name to system-map key instead:
api = Api().using({"db": "async_database"})
# before api.start() is called, System does the equivalent of:
# api.db = system_map["async_database"]
component.dependencies is a dict[str, str] (attribute name -> system-map key)
populated by using(). Dependencies are automatically resolved and components are
started in dependency order; injection happens once, right before start() runs.
Accessing components from outside the system
Components that are part of the system get their dependencies injected as attributes
automatically. Code that lives outside the system — such as a web request handler —
can look a component up by name with system.get_component(name):
from fastapi import Request
@app.get("/users/{user_id}")
def get_user(user_id: str, request: Request):
database = request.app.state.system.get_component("database")
return database.fetch_user(user_id)
System
A system is a collection of components with declared dependencies. The System class:
- Builds a dependency graph from component declarations
- Performs topological sorting to determine initialization order
- Starts components in dependency order
- Shuts down components in reverse dependency order
- Detects circular dependencies and raises an error
Async components
Components may define start()/shutdown() as regular functions or as async def.
The System detects which is which and awaits async ones automatically — you can
freely mix sync and async components in the same system.
class AsyncDatabase(Component):
def __init__(self, dsn: str):
super().__init__()
self.dsn = dsn
self.pool = None
async def start(self):
self.pool = await create_pool(self.dsn)
async def shutdown(self):
await self.pool.close()
The canonical async entry points are await system.astart() and await system.ashutdown().
system.start()/system.shutdown() (no a prefix) still work, with behavior that
depends on the system's contents:
| System contents | Calling system.start() / system.shutdown() |
|---|---|
| Only sync components | Runs entirely synchronously; never touches asyncio. |
| Has async components, no event loop running | Drives the coroutine with asyncio.run(...) for you. |
| Has async components, event loop already running | Raises RuntimeError telling you to await system.astart() / await system.ashutdown() instead. |
In an async application (e.g. a FastAPI app), use astart()/ashutdown() directly, or
use the system as an async context manager:
from contextlib import asynccontextmanager
from fastapi import FastAPI
system = System({
"database": AsyncDatabase(dsn="postgresql://localhost/mydb"),
"cache": Cache().using(["database"]),
})
@asynccontextmanager
async def lifespan(app: FastAPI):
async with system: # calls await system.astart() / await system.ashutdown()
app.state.system = system
yield
app = FastAPI(lifespan=lifespan)
Error handling
- Start failure: if a component's
start()raises, theSystemshuts down every component that had already started, in reverse order, then raisesComponentStartError. The failing component's name is available aserror.component_name, the original exception aserror.__cause__, and any exceptions raised while rolling back aserror.rollback_errors. - Shutdown failure:
shutdown()/ashutdown()always attempts every started component, even if some raise. If any fail, anExceptionGroupcontaining all of the shutdown exceptions is raised after every component has had a chance to clean up. - Cycles: a circular dependency raises
CycleError(re-exported from the standard library'sgraphlib; it's a subclass ofValueError). - Missing dependency: referencing a system-map key that doesn't exist raises
KeyError. - Injection safety: all injections are validated before any component is mutated, so
a failure never leaves the system partially injected. Injecting under a name that
collides with a class attribute or method, a framework-reserved attribute
(
dependencies; plussystem_map/state/_startedon a nestedSystem), or a target a__slots__-restricted component doesn't declare raisesAttributeError. - Sync/async mismatch: on the pure-sync path, a plain
def start()/shutdown()that returns an awaitable raisesTypeErrorinstead of silently dropping it — declare the method withasync defso theSystemawaits it. - Context managers and exceptions: if the
withblock raises and shutdown also fails, the original exception still propagates; the shutdown failure is attached to it as a note instead of replacing it. - State and restart:
system.stateis aSystemStateenum (SystemState.INITIALIZED,SystemState.STARTED,SystemState.STOPPED). Starting an already-started system raisesRuntimeError. Shutdown is idempotent — calling it on a system that isn't started is a no-op. A stopped system can be started again (restart).
from python_components import ComponentStartError, CycleError
try:
system.start()
except ComponentStartError as exc:
print(f"{exc.component_name} failed to start: {exc.__cause__}")
for rollback_exc in exc.rollback_errors:
print(f" rollback also failed: {rollback_exc!r}")
except CycleError as exc:
print(f"circular dependency: {exc}")
Example: Complete Application
from python_components import Component, System
class Database(Component):
def __init__(self, connection_string: str):
super().__init__()
self.connection_string = connection_string
self.connection = None
def start(self):
self.connection = create_connection(self.connection_string)
print("✓ Database connected")
def shutdown(self):
self.connection.close()
print("✓ Database disconnected")
class Cache(Component):
def __init__(self):
super().__init__()
self.data = {}
def start(self):
print("✓ Cache initialized")
def shutdown(self):
self.data.clear()
print("✓ Cache cleared")
class ApiService(Component):
def __init__(self):
super().__init__()
self.server = None
def start(self):
self.server = start_api_server()
print("✓ API service started")
def shutdown(self):
self.server.stop()
print("✓ API service stopped")
# Compose the system
system = System({
"database": Database("postgresql://localhost/mydb"),
"cache": Cache().using(["database"]),
"api": ApiService().using(["database", "cache"]),
})
# Lifecycle management: System is a context manager, so start/shutdown
# are handled for you, including on exceptions.
with system:
pass # Application logic here
# Equivalent, if you need more control over the shutdown path:
try:
system.start()
# Application logic here
finally:
system.shutdown()
Features
- ✨ Simple API: Just implement
start()andshutdown() - 🔗 Explicit Dependencies: Clear, declarative dependency management, injected as attributes
- 📊 Automatic Ordering: Topological sorting ensures correct initialization order
- 🔄 Lifecycle Management: Consistent start/shutdown across all components, with restart support
- ⚡ Async Support: Mix sync and async components;
astart()/ashutdown()and context managers included - 🛟 Automatic Rollback: A failed start cleanly shuts down whatever already started
- 🚨 Cycle Detection: Catches circular dependencies at runtime
- 📦 Zero Dependencies: No runtime dependencies, built on the standard library
- 🐍 Pythonic & Typed: Uses type hints, ships
py.typed, and follows Python best practices
Why Use Components?
Problem: Global State and Initialization Order
Without a component system, applications often struggle with:
- Global mutable state scattered throughout the codebase
- Unclear initialization order leading to subtle bugs
- Difficulty testing components in isolation
- Complex shutdown logic that's easy to forget
Solution: Managed Components
The Component pattern provides:
- Explicit lifecycle: Every stateful resource has clear start/shutdown methods
- Dependency injection: Components receive their dependencies explicitly
- Testability: Easy to create test doubles and inject them
- Reliability: Guaranteed correct initialization and cleanup order
Development
Running Tests
This project uses pytest for testing. To run the tests:
uv run pytest
To run tests with coverage:
uv run pytest --cov=python_components --cov-report=term-missing
Installing Development Dependencies
uv add pytest ruff --group=dev
Requirements
- Python >= 3.11
- No runtime dependencies
License
MIT License - see LICENSE file for details
Acknowledgments
This library is inspired by Alexandra Sierra's Component library for Clojure, which pioneered the pattern of explicit lifecycle management and dependency injection using immutable data structures.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file python_components-0.4.0.tar.gz.
File metadata
- Download URL: python_components-0.4.0.tar.gz
- Upload date:
- Size: 19.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c51e10c35ca27a652d4419d88f541de12665480f92fd8ecb2f888390a2950847
|
|
| MD5 |
9723f7d7e59d485647611544d44f1732
|
|
| BLAKE2b-256 |
13d7aa19db942d62969904d3d86fe088552d8d0ee4477ccd16e59b0319423d45
|
Provenance
The following attestation bundles were made for python_components-0.4.0.tar.gz:
Publisher:
publish-pypi.yml on lucassant95/python-components
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
python_components-0.4.0.tar.gz -
Subject digest:
c51e10c35ca27a652d4419d88f541de12665480f92fd8ecb2f888390a2950847 - Sigstore transparency entry: 2121737231
- Sigstore integration time:
-
Permalink:
lucassant95/python-components@79bf966621410b3f1e182278aaee999d77e50acc -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/lucassant95
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@79bf966621410b3f1e182278aaee999d77e50acc -
Trigger Event:
release
-
Statement type:
File details
Details for the file python_components-0.4.0-py3-none-any.whl.
File metadata
- Download URL: python_components-0.4.0-py3-none-any.whl
- Upload date:
- Size: 22.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
37bbf8fee28a3aa40c5f22c57426ecbe6a37574ffe395c44f3c7deed2d12b9c3
|
|
| MD5 |
93d9f02e4812dab0fe474b439d12a4d6
|
|
| BLAKE2b-256 |
ac4f7e1ca09c68da29981dea803cbf84bbe630cfc265e1caf271f8e07068a19f
|
Provenance
The following attestation bundles were made for python_components-0.4.0-py3-none-any.whl:
Publisher:
publish-pypi.yml on lucassant95/python-components
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
python_components-0.4.0-py3-none-any.whl -
Subject digest:
37bbf8fee28a3aa40c5f22c57426ecbe6a37574ffe395c44f3c7deed2d12b9c3 - Sigstore transparency entry: 2121737379
- Sigstore integration time:
-
Permalink:
lucassant95/python-components@79bf966621410b3f1e182278aaee999d77e50acc -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/lucassant95
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@79bf966621410b3f1e182278aaee999d77e50acc -
Trigger Event:
release
-
Statement type: