With our newest 26.4.1 release of the Zügel MCP server we added Python as a second supported language after. This article only explains the Python setup and Python specific features. If you want to learn what Zügel is and what it can do for you, please read our introduction article.
Python Setup
As described in the introduction you need to create an .mcp.json file that tells your agent how to start Zügel. Then start your agent in the root directory of your Python project and ask it to use the generate_config tool to generate the zugel.json file followed by a call to reload_all. After that the agent load the will analyze your project and create the initial baselines. It will inform you about existing cyclic dependencies on the file and directory/package level. If you add architecture definition files it will also check your architectural rules.
The Python version of zugel.json looks like that:
{
"language": "python",
"project": {
"modules": [
{ "name": "widget", "sourceRoots": ["src"] }
],
"generatedPatterns": ["**/*_pb2.py", "**/*_pb2_grpc.py", "**/generated.py"]
},
"arcFiles": ["architecture/Widget.arc"]
}
The configuration for tolerated cyclic dependencies is the same as in Java. To mark generated code you can provide a list of patterns that match generated Python files. This is relevant for cycle analysis. Cyclic dependencies only consisting of generated files are tolerated automatically.
Identifying things
A Python componentId is module/path/to/file — the source file’s location under its source root, with the extension stripped. app/orders/repository is src/app/orders/repository.py in the module app. That is the same rule Zügel uses everywhere; it is just more visible here, because Python has no fully-qualified type name that identifies a file.
locate_fqn still works, and resolves three kinds of name:
| you have | it resolves |
|---|---|
pkg.archive.UUID | a class |
pkg.archive.read_header | a module-level function |
pkg.archive | the module itself — the file |
That third row has no Java counterpart. In Java a file’s identity already is a class name; in Python a module is a file, and pkg.archive is what an import statement actually writes. It is usually the one you want.
Python Attribute Retrievers
Most .arc patterns match on names and paths, and those work identically. Like for Java we offer a few attribute retrievers that will allow you to match components by other criteria like class hierarchy or decorators.
PythonTypeOf — anything deriving from a given base
include "PythonTypeOf: pydantic.BaseModel"
include "PythonTypeOf: airflow.sdk.bases.operator.BaseOperator"
It matches a Python file if any of the classes in the file have a base class matching the Pattern.
Bases outside your project count. pydantic.BaseModel, enum.Enum, builtins.Exception — you can write a rule about all of them, which matters because that is where a framework’s roles usually live. What Zügel cannot do is see through them: it does not parse installed packages, so a class deriving from BaseModel is found, while one deriving from some third-party class that itself derives from BaseModel is not. (Java has the same boundary at the edge of its classpath.)
PythonHasDecorator — anything carrying a given decorator
include "PythonHasDecorator: airflow.decorators.task"
include "PythonHasDecorator: app.get"
This is Python’s answer to JavaHasAnnotation, with two differences.
It is not called PythonHasAnnotation, deliberately: in Python an annotation is a type hint (x: int), so borrowing Java’s word would name the feature after something else entirely.
And it matches decorators on functions as well as classes — which is the whole point. Decorated classes are rare in Python; decorated functions are everywhere, and they are what carries the role: @app.get, @task, @event.listens_for, @registry.mapped.
Both spellings match, and you will want both:
@task, written afterfrom airflow.decorators import task, resolves toairflow.decorators.task— so a rule written that way keeps working after someone writesimport task as t.@app.getresolves to nothing at all, becauseappis an instance, not a type. There is no fully-qualified name to write. So Zügel also matches the decorator exactly as written, andPythonHasDecorator: app.getdoes what it looks like.
Known limits
Worth knowing before you start, so nothing surprises you:
- Dynamic Python is invisible, and says so. A module using PEP 562’s lazy
__getattr__to expose names — Airflow’s top-level package is the well-known example — cannot be resolved statically by anything. Those references are reported asunresolvedrather than silently dropped. - The class hierarchy stops at your project’s edge, as described above.