brownie-to-ape
Hybrid AST + AI Codemod for migrating Python Ethereum projects from
Brownie to
Ape Framework.
Brownie's README says Brownie is no longer actively maintained and points Python
Ethereum developers toward Ape Framework. brownie-to-ape turns that migration
into a repeatable Codemod workflow: seven deterministic jssg / ast-grep
transforms handle the safe majority of code changes first, then one constrained
AI step handles only documented edge cases that remain.
Real-World Result
brownie-to-ape was tested on three public Brownie repositories:
| Repository | Files changed | Brownie patterns before | Brownie patterns after | Automated | Remaining |
|---|---|---|---|---|---|
| smartcontractkit/chainlink-mix | 19 | 77 | 8 | 89.6% | 10.4% |
| PatrickAlphaC/brownie_simple_storage | 4 | 9 | 0 | 100.0% | 0.0% |
| PatrickAlphaC/brownie_fund_me | 6 | 5 | 1 | 80.0% | 20.0% |
| Combined | 29 | 91 | 9 | 90.1% | 9.9% |
Metric basis: Brownie-specific Python/YAML signatures before and after the
workflow, excluding generated build artifacts. The measured result is 80%+
automated on real projects, with remaining work concentrated in dynamic
wrappers, legacy web3.eth.contract(...) event filters, Brownie conversion
helpers, and project-specific exception handling.
Full run notes are in:
One-Command Usage
Run against an existing Brownie project clone:
bash
Or run directly from the registry:
bash
What It Migrates
- Brownie imports to Ape imports
- accounts[0], accounts.add(...), Account.from_key(...), and LocalAccount
- Contract.at(...), Contract.from_abi(...), project contract containers, interfaces, and deploy calls
- Brownie transaction dictionaries like {"from": account} to Ape keyword arguments such as sender=account
- network.show_active(), network.connect(...), web3.eth.*, and chain-id patterns
- Brownie pytest helpers, isolation fixtures, event dictionaries, brownie.reverts, and brownie.test.strategy
- project.load(...), run(...), Click entrypoints, and Brownie script helpers
- brownie-config.yaml / brownie-config.yml to Ape-style config
Before / After: simple_storage
From PatrickAlphaC/brownie_simple_storage:
diff
Test deployment example:
diff
Before / After: chainlink-mix
From smartcontractkit/chainlink-mix:
diff
Config migration example:
diff
How It Works
The workflow is deliberately hybrid:
- Deterministic transforms run first on Python and YAML files using Codemod
jssg with ast-grep. - Each deterministic transform owns a narrow migration category and avoids
broad rewrites. - A single Codemod built-in AI step runs last, after the safe mechanical
changes are already applied. - The AI step is constrained by a system prompt that requires official Ape docs,
forbids invented APIs, preserves business logic, and leaves short
TODO(brownie-to-ape) comments when project context is required.
Deterministic transform order:
- src/transforms/imports.ts
- src/transforms/accounts.ts
- src/transforms/contracts.ts
- src/transforms/networks.ts
- src/transforms/testing.ts
- src/transforms/project-cli.ts
- src/transforms/config-yaml.ts
The final AI step only handles the remaining edge cases:
- complex custom deployment scripts
- legacy web3.py / Brownie wrapper code
- non-standard account and contract patterns
- heavy pytest mocking
- remaining deprecated Brownie APIs after deterministic transforms
See src/ai-edge-cases.md for examples of what the AI
step should and should not touch.
Screenshots / Diff Placeholders
Use these slots in the hackathon submission or Codemod Registry listing:
| Asset | Placeholder | Capture command |
|---|---|---|
| Dry-run overview | docs/screenshots/dry-run-simple-storage.png | npx codemod workflow run -w workflow.yaml --target /tmp/brownie-simple-storage-demo --dry-run --allow-dirty |
| Chainlink diff stat | docs/screenshots/chainlink-mix-diff-stat.png | cd /tmp/chainlink-mix-brownie-to-ape-2 && git diff --stat |
| Simple Storage deploy diff | docs/screenshots/simple-storage-deploy-diff.png | cd /tmp/brownie-simple-storage-brownie-to-ape && git diff -- scripts/deploy.py |
| Combined metrics table | docs/screenshots/real-repo-metrics.png | Open real-repo-test-results.md and capture the metrics table |
Demo Commands
Chainlink mix dry-run:
bash
Simple Storage dry-run and apply:
bash
Expected changes:
- imports move from Brownie to Ape
- deployment calls use project.ContractName.deploy(..., sender=account)
- local accounts move toward accounts.test_accounts[index]
- private-key loading is converted to accounts.load(...) with manual import guidance
- network checks move from network.show_active() to networks.provider.network.name
- Brownie config receives an Ape-style starter config
- ambiguous dynamic wrappers remain for review instead of being guessed
Development
Install and validate:
bash
Run the workflow against fixtures:
bash
Current local validation:
text
Publishing
Validate before publishing:
bash
Publish to Codemod Registry:
bash
References
- Ape Framework docs: https://docs.apeworx.io/ape
- Ape quickstart: https://docs.apeworx.io/ape/stable/userguides/quickstart
- Ape accounts guide: https://docs.apeworx.io/ape/stable/userguides/accounts.html
- Ape contracts guide: https://docs.apeworx.io/ape/stable/userguides/contracts.html
- Ape networks guide: https://docs.apeworx.io/ape/stable/userguides/networks.html
- Ape scripts guide: https://docs.apeworx.io/ape/stable/userguides/scripts.html
- Ape testing guide: https://docs.apeworx.io/ape/latest/userguides/testing.html
- Brownie deprecation notice: https://github.com/eth-brownie/brownie#readme
- Codemod workflow reference: https://docs.codemod.com/cli/packages/building-workflows
Screenshots
Dry-run diff stat

Deploy script migration

Test file migration

Config migration

Why the AI step made zero edits
In the smoke tests on chainlink-mix, brownie_simple_storage, and brownie_fund_me, the AI step made zero edits. This is the intended behavior: the seven deterministic transforms were comprehensive enough to handle the safely rewritable Brownie patterns in these repos. The AI step activates only when documented Ape equivalents exist for patterns the deterministic transforms intentionally skip, such as dynamic wrappers, custom account classes, complex event dictionary usage, or legacy web3.py event filters. Its zero-edit result is a signal of deterministic coverage, not a misconfiguration.