Lauren vs BlackSheep
The honest, technical comparison for teams choosing their Python backend stack.
“Speed-first Python web framework inspired by ASP.NET Core”
Lineage: ASP.NET Core
Sweet spot: Speed-first apps; teams comfortable with Microsoft-style DI
“Enterprise Python, built to survive the on-call rotation”
Lineage: Axum + NestJS + FastAPI
Sweet spot: Multi-team services; long-lived codebases; audit-heavy environments
Same endpoint, two approaches
A /users/{id} endpoint with a repository, a 404 envelope, and a logger. Same result — different mental model.
from lauren import (
LaurenFactory, controller, get, module, injectable, Path,
)
from lauren.exceptions import HTTPError
from lauren.logging import Logger
class UserNotFound(HTTPError):
status_code = 404
code = "user_not_found"
@injectable()
class UserRepo:
def get(self, id: int): ...
@controller("/users")
class UserController:
def __init__(self, repo: UserRepo, log: Logger) -> None:
self.repo, self.log = repo, log
@get("/{id}")
async def show(self, id: Path[int]) -> dict:
user = self.repo.get(id)
if user is None:
self.log.warn(f"user {id} not found")
raise UserNotFound("user does not exist", detail={"id": id})
return {"id": user.id, "name": user.name}
@module(controllers=[UserController], providers=[UserRepo])
class AppModule: ...
app = LaurenFactory.create(AppModule)Key differences
Each row corresponds to a real source of bugs in production services.
| Capability | BlackSheep | Lauren |
|---|---|---|
| Module system | None — services registered directly on app.services. No imports/exports boundary; any controller can consume any registered service. | @module(imports=[...], exports=[...]) — providers are reachable only if declared or transitively re-exported. ModuleExportViolation raised at startup. |
| DI model | ASP.NET Core-style IoC container (add_singleton / add_scoped / add_transient). Scope violations are not always enforced at startup. | NestJS-style with 3 scopes (SINGLETON / REQUEST / TRANSIENT) + compile-time DIScopeViolationError before any request is served. |
| Startup validation | More startup checks than FastAPI, but module export violations and some scope issues are not modeled. | Full 7-phase pipeline. Every misconfiguration has a named StartupError subclass raised before the process accepts traffic. |
| Error contract | Limited built-in error classes; error envelope shape is constructed manually per service with no shared convention. | 28 documented error classes with stable code strings. Every error renders as {"error": {"code": "...", "message": "...", "detail": {...}}}. |
| Structured logging | Basic logging support; per-request structured format not standardized or auto-leveled by status code. | Built-in ConsoleLogger / JsonLogger. Per-request traces auto-leveled: DEBUG 2xx, WARN 4xx, ERROR 5xx. Ships to Splunk / Datadog immediately. |
| Custom providers | add_singleton / add_scoped / add_transient. No factory pattern with async support; no use_existing alias. | use_value / use_class / use_factory / use_existing. Async factories awaited automatically. Environment-conditional provider swaps. |
| Inheritance semantics | Implicit — subclassing a BlackSheep Controller can silently propagate route metadata to the child class. | Strict — MetadataInheritanceError at startup prevents accidental controller or injectable registration via inheritance. |
| AI-ready docs | No llms.txt bundled with the package. | Ships llms.txt and llms-full.txt (~25 KB). Paste into Cursor / Claude Code / Copilot for idiomatic Lauren on the first try. |
When do bugs surface?
A startup error is a CI failure. A runtime error is a 3 a.m. page. Lauren catches every configuration bug before traffic flows.
| Bug class | BlackSheep | Lauren |
|---|---|---|
| Route-path conflict | Errored | RouterConflictError at startup |
| DI cycle (A → B → A) | Errored at first resolve | CircularDependencyError at startup |
| Missing provider | First-request 500 | MissingProviderError at startup |
| Two providers for same Protocol | Sometimes errored | ProtocolAmbiguityError at startup |
| Scope violation (singleton ← request) | Sometimes errored | DIScopeViolationError at startup |
| Module export violation | N/A — concept not modeled | ModuleExportViolation at startup |
| Subclass accidentally registered | Possible, silent | MetadataInheritanceError at startup |
Final scorecard
A subjective summary — what we'd tell a colleague picking their stack.
| Criterion | BlackSheep | Lauren |
|---|---|---|
| Time-to-first-endpoint | Good | Fair |
| Solo dev ergonomics | Good | Fair |
| 5-person team ergonomics | Good | Excellent |
| 50-person team ergonomics | Good | Excellent |
| Type safety end-to-end | Good | Excellent |
| Startup-time validation | Good | Excellent |
| Production logging out of the box | Fair | Excellent |
| Graceful shutdown semantics | Good | Excellent |
| Stable error contract | Fair | Excellent |
| Multi-team module discipline | Fair | Excellent |
| Audit-friendliness | Good | Excellent |
| Raw runtime performance | Excellent | Excellent |
| Ecosystem & docs (today) | Good | Fair |
Which should you pick?
Choose BlackSheep when…
- Team comes from ASP.NET Core and wants familiar Microsoft-style DI patterns
- Raw throughput performance is the single most important criterion
- Smaller service without multi-team module boundary requirements
- You prefer the ASP.NET Core controller inheritance model
Choose Lauren when…
- Multi-team codebase requiring NestJS-style explicit module boundaries
- DI scope violations must be a CI failure, not a 3 a.m. production incident
- Audit-heavy service requiring stable error codes and structured JSON logs
- Service that must handle graceful shutdown in topological order with bounded timeouts
- Migrating from NestJS and want the exact same DI and module mental model in Python
Ready to start?
Get your first Lauren service running in minutes — and experience the startup-validation difference first-hand.