Production-grade service locator and dependency injection container for Python with thread-safe singletons, lazy loading, and factory patterns
Project description
GetIt: Production-Grade Service Locator & Dependency Injection Container
GetIt is a thread-safe, production-ready service locator and dependency injection container for Python. It provides elegant, decoupled architecture through declarative registration, multiple resolution strategies, and automatic lifecycle management—all without the boilerplate.
Designed for WSGI/ASGI applications, microservices, and complex Python projects that need flexible, maintainable dependency management.
Features
- Thread-Safe: Double-checked locking guarantees singletons instantiate exactly once under concurrent load
- Multiple Lifecycles: Singleton, Lazy Singleton, and Factory patterns for flexible dependency management
- Zero-Import Resolution: Fetch dependencies at runtime without circular imports
- Hybrid Resolution APIs: Class-based, dot-notation, and explicit getter syntaxes
- Declarative Registration: Use
@Singleton,@LazySingleton,@Factorydecorators at point of definition - Full Type Hints: Complete type annotations with
@overloadfor IDE autocomplete in VS Code and PyCharm - Production Logging: Structured logging for observability and debugging
- Comprehensive Error Handling: Custom exceptions with actionable error messages
Quick Start
Installation
pip install getit-python
Basic Example
from get_it import GetIt, singleton
@singleton
class Service:
def sync(self):
print("Syncing.....")
# Global resolution
service = GetIt.get(Service)
service.sync() # Output: Syncing.....
Complete Usage Guide
1. Defining Services with Decorators
Services are registered where they're defined, keeping code organized and discoverable.
Singleton (Eager Initialization)
Instantiated immediately at decorator evaluation time. Use for lightweight config or static resources.
from get_it import GetIt, singleton
@singleton
class AppConfig:
def __init__(self):
self.debug = True
self.host = "localhost"
self.port = 8000
print("[CONFIG] Initialized at import time")
# Automatically registered and instantiated
config = GetIt.get(AppConfig)
print(config.host) # "localhost"
Lazy Singleton (Deferred Initialization)
Instantiated only on first access. Perfect for expensive operations like database connections.
from get_it import GetIt, LazySingleton
@LazySingleton
class DatabaseService:
def __init__(self):
print("[DB] Connecting to database...")
self.connected = True
def query(self, sql: str):
return f"Executing: {sql}"
# Not instantiated yet
print("App starting...")
# First access triggers initialization
db = GetIt.get(DatabaseService) # Prints: [DB] Connecting to database...
result = db.query("SELECT * FROM users")
Factory (Fresh Instance Per Request)
Creates a new instance every time. Ideal for request-scoped state or temporary objects.
from get_it import GetIt, Factory
@Factory
class RequestContext:
def __init__(self):
self.request_id = id(self)
self.user_data = {}
# Each call gets a fresh instance
ctx1 = GetIt.get(RequestContext)
ctx2 = GetIt.get(RequestContext)
assert ctx1 is not ctx2 # Different instances
assert ctx1.request_id != ctx2.request_id
2. Resolution Strategies
GetIt supports multiple ways to resolve dependencies. Choose the pattern that best fits your codebase.
Strategy A: Dot-Notation (Cleanest)
Flutter-style syntax. Resolves by class name matching.
from get_it import GetIt, LazySingleton
get_it = GetIt()
@LazySingleton
class DatabaseService:
def connect(self) -> None:
print("--> Establishing Database Connection...")
@LazySingleton
class UserRepository:
def get_user(self, user_id: int):
return {"id": user_id, "name": "Alice"}
# Dot-notation resolution
db: DatabaseService = get_it.DatabaseService
db.connect()
repo: UserRepository = get_it.UserRepository
user = repo.get_user(1)
print(user)
Strategy B: Instance Call
Pass the type directly to the locator instance.
from get_it import GetIt, LazySingleton
get_it = GetIt()
@LazySingleton
class CacheService:
def set(self, key: str, value: str):
print(f"Cache: {key} = {value}")
# Instance call syntax
cache = get_it(CacheService)
cache.set("user_123", "cached_data")
Strategy C: Metaclass Resolution
Call the class directly for explicit type-based resolution.
from get_it import GetIt, LazySingleton
@LazySingleton
class AuthService:
def verify_token(self, token: str) -> bool:
return len(token) > 10
# Metaclass resolution
auth = GetIt(AuthService)
is_valid = auth.verify_token("my_token_12345")
print(f"Valid: {is_valid}")
Strategy D: Explicit Get
Standard, unambiguous method for any situation.
from get_it import GetIt, LazySingleton
@LazySingleton
class MailService:
def send(self, to: str, message: str):
print(f"Email to {to}: {message}")
# Explicit get
mail = GetIt.get(MailService)
mail.send("user@example.com", "Hello!")
Complete Example with All Strategies
from get_it import GetIt, LazySingleton
get_it = GetIt()
@LazySingleton
class DatabaseService:
def connect(self) -> None:
print("--> Establishing Database Connection...")
def handle_user_login():
# Strategy A: Dot-notation (cleanest)
db_a: DatabaseService = get_it.DatabaseService
db_a.connect()
# Strategy B: Instance call
db_b = get_it(DatabaseService)
db_b.connect()
# Strategy C: Metaclass call
db_c = GetIt(DatabaseService)
db_c.connect()
# Strategy D: Explicit getter
db_d = GetIt.get(DatabaseService)
db_d.connect()
# All resolve to the same cached instance
assert db_a is db_b is db_c is db_d
handle_user_login()
3. Real-World Web Framework Integration
Flask Example
from flask import Flask, jsonify
from get_it import GetIt, LazySingleton
app = Flask(__name__)
get_it = GetIt()
@LazySingleton
class UserDatabase:
def __init__(self):
print("[INIT] Connecting to PostgreSQL...")
# self.connection = psycopg2.connect(...)
def get_user(self, user_id: int):
return {"id": user_id, "name": "Alice", "email": "alice@example.com"}
@LazySingleton
class AuthService:
def __init__(self):
self.db = get_it.UserDatabase
def authenticate(self, user_id: int):
user = self.db.get_user(user_id)
return bool(user)
@app.route("/users/<int:user_id>")
def get_user(user_id: int):
auth = GetIt.get(AuthService)
if auth.authenticate(user_id):
user = auth.db.get_user(user_id)
return jsonify(user)
return jsonify({"error": "Unauthorized"}), 401
if __name__ == "__main__":
app.run(debug=True)
FastAPI Example
from fastapi import FastAPI, HTTPException
from get_it import GetIt, LazySingleton
app = FastAPI()
get_it = GetIt()
@LazySingleton
class UserService:
async def get_user(self, user_id: int):
# Simulated database call
return {"id": user_id, "name": "Bob", "email": "bob@example.com"}
@app.get("/users/{user_id}")
async def read_user(user_id: int):
service = GetIt.get(UserService)
user = await service.get_user(user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
4. Manual Registration
For runtime registration or testing scenarios:
from get_it import GetIt
class Logger:
def log(self, msg: str):
print(f"[LOG] {msg}")
# Manual registration
logger = Logger()
GetIt.register_singleton(logger)
# Resolution
retrieved_logger = GetIt.get(Logger)
retrieved_logger.log("Hello from manual registration!")
# Factory registration
GetIt.register_factory(Logger, lambda: Logger())
# Lazy singleton registration
GetIt.register_lazy_singleton(Logger, lambda: Logger())
5. Testing with GetIt
Isolate tests by resetting the container and registering mocks:
import pytest
from get_it import GetIt
class UserRepository:
def get_user(self, user_id: int):
# Real implementation
return {"id": user_id, "name": "Real DB"}
class UserService:
def __init__(self):
self.repo = GetIt.get(UserRepository)
def fetch_user(self, user_id: int):
return self.repo.get_user(user_id)
@pytest.fixture
def clean_di():
"""Clean GetIt before and after each test."""
GetIt.reset()
yield
GetIt.reset()
@pytest.fixture
def mock_user_repo(clean_di):
"""Provide a mock repository for testing."""
class MockRepository:
def get_user(self, user_id: int):
return {"id": user_id, "name": "Mock User", "test": True}
mock = MockRepository()
GetIt.register_singleton(mock)
return mock
def test_user_service_with_mock(mock_user_repo):
service = UserService()
user = service.fetch_user(1)
assert user["name"] == "Mock User"
assert user["test"] is True
Lifecycle Reference
| Lifecycle | When Initialized | When Destroyed | Best For |
|---|---|---|---|
| Singleton | At decoration/registration time | Application lifetime | Static config, caches, shared state |
| LazySingleton | On first access | Application lifetime | Database connections, API clients, services |
| Factory | On every access | Immediately after use | Request-scoped state, temporary objects |
Exception Handling
GetIt provides specific exceptions for precise error handling:
from get_it import (
GetIt,
DependencyNotRegisteredError,
InvalidDependencyTypeError,
LazySingleton
)
@LazySingleton
class Service:
pass
try:
# This will raise DependencyNotRegisteredError
GetIt.get(str)
except DependencyNotRegisteredError as e:
print(f"Not registered: {e}")
try:
# This will raise InvalidDependencyTypeError
GetIt.register_lazy_singleton("not_a_type", lambda: None)
except InvalidDependencyTypeError as e:
print(f"Invalid type: {e}")
Performance Characteristics
| Operation | Complexity | Notes |
|---|---|---|
GetIt.get(Type) singleton |
O(1) | Direct dict lookup, no lock |
GetIt.get(Type) lazy (first) |
O(1) + factory | Uses double-checked locking |
GetIt.get(Type) lazy (cached) |
O(1) | Direct dict lookup, no lock |
GetIt.get(Type) factory |
O(1) + factory | Creates new instance |
get_it.ServiceName |
O(n) | Linear search by name, n=registered types |
Tip: For performance-critical paths, prefer explicit type-based resolution (GetIt.get(Type)) over dot-notation to avoid the O(n) name lookup.
Best Practices
Do
- Register services at their definition point using decorators
- Use
LazySingletonfor expensive operations (database, HTTP clients) - Use
Factoryfor request-scoped or stateful objects - Type-hint all resolved variables for IDE autocomplete
- Call
GetIt.reset()in test teardown to prevent test pollution - Use logging to debug dependency resolution (enable DEBUG logs)
Don't
- Create circular dependencies (register A that depends on B that depends on A)
- Block on expensive I/O in
@singletonconstructors (use@LazySingletoninstead) - Resolve dependencies in module-level code (defer to runtime)
- Mix service registration across multiple files without clear organization
- Register services inside request handlers (register during app startup)
Comparison with Alternatives
| Feature | GetIt | dependency-injector | injector | manual DI |
|---|---|---|---|---|
| Thread Safety | Yes (RLock) | Yes | Partial | N/A |
| Lazy Singletons | Yes | Yes | Yes | Manual |
| Factory Pattern | Yes | Yes | Yes | Manual |
| Dot-Notation | Yes | No | No | N/A |
| Type Hints | Full | Partial | Partial | Full |
| Decorator Support | Yes | Yes | Yes | No |
| Learning Curve | Low | Medium | Low | High |
| Circular Import Safe | Yes | Yes | Yes | No |
Contributing
Contributions are welcome! Please ensure:
- All tests pass:
python3 -B test_get_it.py - Code is type-checked:
mypy get_it.py - Follow PEP 8 style guidelines
License
MIT License - see LICENSE file for details
Changelog
v1.0.0 (2026-06-18)
- Initial release
- Thread-safe singleton, lazy singleton, and factory patterns
- Comprehensive type hints and error handling
- Full documentation and test coverage
Project details
Release history Release notifications | RSS feed
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 getit_python-1.0.0.tar.gz.
File metadata
- Download URL: getit_python-1.0.0.tar.gz
- Upload date:
- Size: 25.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b1c12040a3426fdaffa92dc55ef173b9c7ef744f923274d1217f51263688caf9
|
|
| MD5 |
0e353df6f504861bc1a165142949e507
|
|
| BLAKE2b-256 |
aa16984a994d5ead806d9888b3f1ac279a706e8334fed5d0daeeec8b8a438670
|
File details
Details for the file getit_python-1.0.0-py3-none-any.whl.
File metadata
- Download URL: getit_python-1.0.0-py3-none-any.whl
- Upload date:
- Size: 15.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ac49e54cdce6a8364f0e06a5b1afaef7520147236f4f5cf6c5e68dae25bcf9c1
|
|
| MD5 |
04117edeb49a84997b0f5e1cac828f96
|
|
| BLAKE2b-256 |
765a98ba87a668cbbb3a1f947fa4910746603e264434141b4918adb1cb1fdad1
|