Packages Examples Agents Blog Get started
← Blog

I built a Python framework inspired by Laravel

Laravel taught me what developer experience feels like. Here is how those instincts live in Python.

Laravel has a way of making backend architecture feel effortless: service providers that wire dependencies, a container that resolves them, facades that give you ergonomic access, and a shelf of services (queues, events, cache, auth) already integrated. I borrowed those instincts — and spent a long time making them feel at home in Python.

This post is the honest accounting: what I copied, where Python asked for a different shape, and the rules that keep Laravel’s DX intact once everything is importable.

What I borrowed from Laravel

Laravel’s service provider was the first architecture that made dependencies feel solved, so I carried its DNA into Oridecon:

  • Service providers → providers with priorities. The register() / boot() lifecycle, ordered by ProviderPriority.
  • The containercontainer.singleton(Protocol, Impl), resolved once, torn down in reverse priority.
  • Contracts — Laravel ships a laravel/contracts package; Oridecon elevated the idea into a law with a machine enforcing it.
  • Facades → ambient capabilities. clock.now(), identity.new_uuid(), hashing.hash_hex() keep Facades’ ergonomics — call it from anywhere — with the indirection made explicit: plain objects, injectable, overridable in tests.
  • Artisan → the CLI, with generators and contributors.
  • The same shelf of layers: queues, events, notifications, cache, file storage, auth gates — your PermissionService.can_list() is a direct descendant of Laravel’s Gate.

How those instincts became laws

None of this is a dig at Laravel — the port is gratitude, not criticism. Laravel’s looser boundaries are a rational adaptation to PHP’s runtime, which absorbs ordering mistakes with a shrug. Python is the opposite: everything is importable, and nothing catches you when you trip. The way to keep Laravel’s DX intact there is to write the habit down as a type, a linter rule, or a CI gate. Three decisions show the pattern:

1. register() cannot resolve. Period. In Laravel a provider touches the container while registering — a freedom that works there. Ported to Python it quietly becomes ordering bugs, so Oridecon removes the freedom entirely by typing: register() receives ContainerRegistrarProtocol, boot() receives BootContainerProtocol — and the two never meet in the same hand.

class NotificationProvider(Provider):
async def register(self, container: ContainerRegistrarProtocol) -> None:
container.singleton(NotificationServiceProtocol, SlackNotifier)
async def boot(self, container: BootContainerProtocol) -> None:
notifier = await container.resolve(NotificationServiceProtocol)

A provider that tries to resolve during registration doesn’t fail at review; it fails type checking (with mypy or pyright, in CI, before a human ever reviews the PR). The rule isn’t advice — it’s a compile error.

2. Boundaries are law, enforced by a machine. Python’s import system is a free-for-all: oridecon-web reaching into oridecon-sql compiles fine and rots silently. So the framework has a Golden Rule written as an absolute: if two or more packages need the same type, protocol, or exception, it lives in oridecon-contracts. No exceptions. An import linter enforces six architectural contracts and blocks the PR, and there’s a decision tree for where a protocol goes — because “err on the side of contracts” is not a slogan, it’s a checklist. Even the carve-outs are documented and deliberate: admin may import from auth, cache, and resilience, because dashboards need them — and those exceptions are named, not implicit. A boundary you can’t justify is a boundary you don’t get.

3. Expected failures are values. Infrastructure failures raise. The most stealable idea in the codebase is a decision table with one column called “what the caller is expected to handle.” User not found is a return value; the database dying is an exception. Then the bans: no unwrap() without an is_ok() check, no Result from constructors or lifecycle hooks, no Any as the error type. A type that tells you which failures you must handle changes how the whole codebase reads.

Security defaults that fail closed

Security isn’t a feature list — it’s the sum of every default. The framework’s rules are written fail-closed: an auth check that can’t prove the caller is allowed rejects and logs, RBAC closes in the face of ambiguity, and the admin surface treats an unverified identity as a hostile one. There is no “probably fine” path in the code — the only way through is proof.

That fail‑closed posture is enforced, not just encouraged. Every security decision is part of the framework’s public audit framework, tracked in a per‑area status document that ships with each release. If a rule can’t be proven, it doesn’t exist. You can read the full audit on the docs audit pages — it’s honest, and it tells you exactly how exposed you are.

The punch worth stealing: an API that defaults to yes optimizes for the demo. Fail-closed optimizes for the 3 a.m. audit — and that’s the one that matters.

Other laws worth stealing

  • Registries never self-register. An empty constructor and a with_defaults() classmethod; dispatch through a registry, never if/elif chains.
  • Facades with honest overrides. The ambient clock, identity, and hashing are a documented exception to dependency injection — with a rationale, and a use() context manager so tests can swap them. The ergonomics of Facades, the overrides made explicit.
  • Tests validate the design. “No shims, no ad-hoc code to pass a test — fix the design, not the test.” Fake at the contract boundary; test doubles live in tests/, never in src/.
  • No singletons. Container-managed singletons only. Module-level state is a fire waiting to be lit.

The AI layer follows the same rules

The newest layer carries the same discipline: every oridecon-ai-* package depends only on oridecon and oridecon-contracts — never on each other. Agents, RAG, memory, skills, MCP: they talk through contracts, discovered via entry points, wired by the container. And the rules are written for machines too — there’s a 53KB operating manual in the repo, skills packages that teach Claude Code and opencode the same patterns, docs with an llms.txt. The framework’s theory of itself is a first-class artifact.

If Laravel taught me what great developer experience feels like, Python taught me how to keep it: write the rule, then let the type checker and the import linter hold it so nobody has to remember it at 2 a.m.

Steal this

  • If two codebases need the same type, make a home for shared contracts before the third one shows up — and write the rule down first.
  • Rules that live in READMEs rot. Rules enforced by an import linter, a type checker, or a CI gate are rules.
  • Facades are a good idea — the secret is making the convenience explicit. Export real objects, keep them swappable.
  • Being inspired is not something to hide. Frameworks are conversations with their teachers — and the best thing about Laravel is that it keeps agreeing to be copied.

Oridecon is open source.
Star it on GitHub, read the docs, or install it from PyPI.
If I got the laws wrong — or right — I’d love to hear from you.

I’m building Oridecon because I wanted Laravel’s developer experience with Python’s type safety. You can follow the journey on GitHub.

If you ship FastAPI today, you are not starting over — Coming from FastAPI is the story, and the migration guide is the concept map.