magentic: LLM Calls as Typed Python Functions
Seamlessly integrate LLMs as Python functions
At a glance
- What is it?
- magentic turns prompts into ordinary Python functions whose return values are pydantic models, and lets those functions call each other. It is a good fit for teams already using pydantic and OpenAI-compatible APIs, and a poor fit for anyone who needs a stable API surface across minor versions.
- Who is it for?
- Adopt magentic if your application already expresses its data as pydantic models and you want LLM calls to look like ordinary typed function calls inside that code. Do not adopt it if you need a frozen API surface, since the pre-1.0 version numbers and the 0.40.0 to 0.41.0 jump suggest breaking changes are still normal.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Activity is slowing. The repository last received commits 6 months ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
The problem magentic solves is the gap between a string and a Python value
Most LLM calls in application code end the same way: a string comes back, and you write parsing code around it. If you want a dataclass or a pydantic model out of the model, you describe the schema in the prompt, then validate whatever arrives. magentic removes that step by making the return type annotation the schema. The README example is a function with no body, only a decorated signature: `def create_superhero(name: str) -> Superhero: ...`, where `Superhero` is a pydantic `BaseModel` with `name`, `age`, `power` and `enemies` fields. The decorator reads that annotation and the call returns an instance, not JSON text. The audience is Python developers who already think in type annotations and pydantic models, and who want LLM calls to sit inside that same style rather than beside it. If your codebase has no pydantic usage and no type checking, the library's main advantage mostly disappears.
How @prompt, @chatprompt and @prompt_chain divide the work
There are three decorators and they map to three levels of control. `@prompt` takes a single template string, fills `{phrase}`-style fields from the function arguments, and returns the annotated type. `@chatprompt` takes an ordered list of message objects (`SystemMessage`, `UserMessage`, `AssistantMessage`) instead of one string, which is how the README shows few-shot prompting: an assistant message containing a filled-in `Quote` model is placed before the real question, and the documentation notes that format fields are substituted in all messages except `FunctionResultMessage`. `@prompt_chain` is the one that changes the control flow. Given a prompt and a `functions=[...]` list, it resolves `FunctionCall` objects automatically and feeds the result back to the model until a final answer is produced. The README's weather example shows `get_current_weather` being invoked by the model and its return value, a dict with `temperature` and `forecast`, being folded into the final string. The composable part is that functions created with any of these decorators can themselves be passed in a `functions` list to another decorated function, so a chain can be built from smaller chains that are tested separately.
FunctionCall is the seam between the model and your code
When a decorated function is declared to return `FunctionCall[str]` and given a `functions` list, the model does not execute anything. It picks a function and supplies arguments, and magentic hands you a `FunctionCall` object. The README shows `FunctionCall(<function search_twitter at 0x10c367d00>, 'LLMs', 'latest')` being printed before `output()` is called. That deferred execution is the design decision worth noting: it means you can inspect, log, or refuse the call before it runs, and side effects only happen when you invoke the object yourself. The docstring of each candidate function is what the model sees when choosing, which is why the README examples give every tool a one-line docstring. `@prompt_chain` is the opposite trade: it resolves calls for you, so you lose the inspection point in exchange for not writing the loop.
Installing and configuring a provider
Installation is one command, given as `pip install magentic` or `uv add magentic`. The README states that setting the `OPENAI_API_KEY` environment variable configures OpenAI, and that other providers are covered on the Configuration page, which the feature list names as OpenAI, Anthropic and Ollama. The README does not inline the configuration keys for those providers, so the exact environment variable names for Anthropic or Ollama are not something I can state from this material. Streaming is exposed through `StreamedStr` and `AsyncStreamedStr`, imported from the top-level package alongside `prompt`. Observability is described as OpenTelemetry-based with a native Pydantic Logfire integration, and the feature list also points to pages for Parallel Function Calling, Vision, Formatting, Asyncio and LLM-Assisted Retries. Those pages are links in the README, not content reproduced in it.
What the version history tells you about upgrade cost
The releases listed are v0.41.1 in March 2026, v0.41.0 in October 2025, and v0.40.0 in June 2025. The project is pre-1.0, and the gap between 0.40.0 and 0.41.0 is a minor-version bump that, under semantic versioning conventions for 0.x releases, is where breaking changes are permitted. Nothing in the supplied material documents a deprecation policy or a compatibility guarantee. The practical implication is that pinning a version is the sensible default, and that reading the release notes before a minor upgrade is not optional caution but the expected workflow. The licence is MIT, which is permissive and short; I am not giving legal advice, and anyone redistributing the library inside a product should read the licence text itself rather than a summary.
Where magentic is the wrong choice
The library assumes the model can be constrained to a schema. LLM-Assisted Retries exists precisely because that assumption fails sometimes: the feature list describes it as a mechanism to improve adherence to complex output schemas, which implies that complex schemas produce non-adherent output often enough to need a retry loop. Retries cost tokens and latency, and a deeply nested pydantic model will hit that path more often than a flat one. The second limitation is provider coupling. The README's configuration story begins with `OPENAI_API_KEY`, and the decorators' structured-output behaviour depends on the provider supporting the mechanism magentic uses, so a provider that lacks it is not a drop-in swap. The third is that `@prompt_chain` gives you a loop with no built-in budget: the README example resolves one function call, but nothing in the supplied material describes a maximum iteration count, so a model that keeps requesting tools is a scenario you would need to handle yourself.
How this differs from LangChain
LangChain is the obvious comparison and the difference is architectural rather than feature-by-feature. LangChain is built around composable objects: chains, runnables, and a large library of pre-built integrations that you assemble into a pipeline. magentic is built around the Python function itself. A decorated function is a plain callable with a type annotation, so it can be passed to another decorated function's `functions` list, imported, mocked, or type-checked by an IDE without any framework-specific wrapper. The README makes this explicit when it says decorated functions can be supplied as `functions` to other decorators just like regular Python functions, which it frames as enabling components to be tested in isolation. If your problem is gluing many third-party services and vector stores together, LangChain's integration surface is the point. If your problem is that your own Python code needs typed LLM calls inside it, magentic's smaller surface is the point.
Who should adopt it, and what to check first
The fit is a Python service or script that already uses pydantic for its domain types, uses type checking in CI, and calls an OpenAI-compatible endpoint. In that setting the migration is mechanical: move a prompt template into a decorator, set the return annotation to the model you already have, and delete the parsing layer. The misfit is a project that needs a frozen API, or one whose LLM provider is not among those the Configuration page documents, or one where a wrong structured output is worse than a slow one and the retry path is unacceptable. Before adopting, read the Configuration page for your specific provider rather than assuming the OpenAI path generalises, and check the LLM-Assisted Retries page to understand what happens when a schema is not satisfied. The repository's own README is the shortest path to a decision: the `dudeify`, `create_superhero` and `perform_search` examples are small enough to run against your own key in a few minutes, and that is a better signal than any feature list.
Editorial conclusion
Adopt magentic if your application already expresses its data as pydantic models and you want LLM calls to look like ordinary typed function calls inside that code. Do not adopt it if you need a frozen API surface, since the pre-1.0 version numbers and the 0.40.0 to 0.41.0 jump suggest breaking changes are still normal. Before committing, verify the current Configuration page for your provider, confirm the retry behaviour of LLM-Assisted Retries against your own schema, and check whether the MIT licence terms suit how you intend to redistribute the library.
Community notes