Python obfuscation for AI assistants: runnable workspaces and off-disk secrets A developer building Python obfuscation tools for AI assistants must shift from Java's compile-time validation model to a runtime-dependent approach, as Python lacks a compilation step to catch renamed identifiers. The obfuscator must handle silent contract breaks across frameworks like Pydantic, Flask, and Django, where renamed class names, function names, or field references cause errors only when code executes. To maintain security, the developer implements a `promptcape run` pattern that supplies environment variables at launch time without writing them to disk in the AI-visible workspace. Why obfuscating Python for AI tools requires a different mental model than Java — and how .env handling becomes the load-bearing question. Obfuscating Java for an AI assistant is — at heart — about producing a workspace that still compiles . The developer rarely runs the obfuscated workspace directly; they let the AI work in it, apply the changes back to source, and run the app from there. Compilation is the contract. If mvn test-compile passes after obfuscation, you're 95% done. Python is a fundamentally different game. There is no compile step. The workspace's "validation" happens at runtime , when the developer fires up: streamlit run dashboard.py pytest -v python main.py uvicorn main:app --reload If a framework introspects a class name, a function name, a string in a URL pattern, or a Pydantic field — and that name was rewritten by the obfuscator — the error surfaces only when Python tries to call it. There is no compiler to catch it for you. That changes the obfuscator's job in three concrete ways: --verify step can't compile; it has to do static import resolution and let runtime catch the rest. .env to the workspace instantly defeats the obfuscation's purpose.This article walks through each of those, then explains the promptcape run pattern: how to give the Python workspace the env vars it needs at launch time without ever writing them to disk in the AI-visible location. In Java, framework conventions usually leave a compile-time trace: a missing getName from a Lombok-renamed field throws cannot find symbol at javac time. You can detect it, you can auto-fix it. Spring Data derived queries findByActiveTrue are the rare exception that bites at startup, not at compile time — and that's already documented as a hard case. Python frameworks are full of conventions like Spring Data. Names are silent contracts: | Framework | Identifier | Contract | |---|---|---| Pydantic v2 | class User BaseModel : email: str | email is the JSON key in every user.model dump call. Rename it, every API consumer breaks silently. | Flask | def index : ... decorated with @bp.route "/" | The function name becomes the default endpoint string for url for "blog.index" and {{ url for 'blog.index' }} . Rename it, every redirect and template link 500s with werkzeug.routing.BuildError . | Django | class Post models.Model | The class name drives the DB table app label post AND every migration reference. Rename it, your INSERT query targets a table that doesn't exist. | SQLAlchemy | id = Column Integer, primary key=True | id is the column name on the table. Plus it's accessed as instance.id everywhere. | pytest | def test login succeeds ... | pytest discovers tests by the test prefix. Rename to mtd xxx , pytest collects 0 tests — your CI silently passes with no signal. | dataclass / attrs | @dataclass class Post: title: str | Field names are accessed as obj.title , dumped via asdict and rendered in Jinja templates as {{ post.title }} . | Django forms / DRF serializers | def clean email self : ... | Django discovers field-level validators by the literal clean