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.
- 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
pip install getit-pythonfrom get_it import GetIt, singleton
@singleton
class Service:
def sync(self):
print("Syncing.....")
# Global resolution
service = GetIt.get(Service)
service.sync() # Output: Syncing.....Services are registered where they're defined, keeping code organized and discoverable.
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"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")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_idGetIt supports multiple ways to resolve dependencies. Choose the pattern that best fits your codebase.
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)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")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}")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!")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()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)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 userFor 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())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 | 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 |
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}")| 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.
- 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)
- 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)
| 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 |
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
MIT License - see LICENSE file for details
- Initial release
- Thread-safe singleton, lazy singleton, and factory patterns
- Comprehensive type hints and error handling
- Full documentation and test coverage