PPugarHuda

pugarhuda/brownie-to-ape

Automated migration codemod from Brownie to ApeWorx Ape framework — imports, deploy syntax, transaction kwargs, network/chain API, reverts, conftest fixtures

brownieapeapeworxpythonethereumsmart-contractsmigration
Public
17 executions

Run locally

npx codemod @pugarhuda/brownie-to-ape
<p align="center"> <img src="./docs/logo.svg" alt="brownie-to-ape logo" width="180" /> </p>

brownie-to-ape

<p align="center"> <img src="./docs/banner.svg" alt="brownie-to-ape — 250 tests, 5 OSS repos, 0 false positives, ape test 38/38 passed" width="100%" /> </p>

CI
License: MIT
Tests
FP Rate
Validated repos
ape test
Version
ApeWorX docs PR
Codemod docs PR
Case studies
Mutation
jssg

Automated migration codemod from Brownie to ApeWorx Ape. 17-pass deterministic transform built on Codemod's jssg engine. Validated on 5 real OSS Brownie projects (incl. Yearn Finance) with zero false positives.

🌐 Codemod registry: https://app.codemod.com/registry/@pugarhuda/brownie-to-ape · 🇮🇩 Bahasa Indonesia: README.id.md

Published case studies

Three published write-ups covering the migration from different angles:

Submitted to the Codemod Boring AI hackathon (Track 1: Production Migration Recipes + Track 2: Public Case Study).

Live demo: https://pugarhuda.github.io/brownie-to-ape/
Registry: @pugarhuda/brownie-to-ape (app.codemod.com)

TL;DR

bash

Or:

bash

📋 Hackathon evaluator? See EVALUATOR.md for the
3-step evaluation walkthrough (~15–20 min total): codemod run → AI
cleanup → ape compile && ape test verification. End-to-end AI-step
walkthrough on token-mix: demo/ai-step-demo.md.
Engineering tradeoffs / deferred features:
docs/DEFERRED_FEATURES.md.

Why use this

Brownie was deprecated in 2023; ApeWorX Ape is the recommended successor. A typical Brownie test suite contains 50–200 mechanical pattern rewrites: every Contract.deploy(…, {"from": acct}) becomes Contract.deploy(…, sender=acct), every network.show_active() becomes networks.active_provider.network.name, etc.

Manual migrationbrownie-to-ape
Time per repo (typical)half-day to 1 day~30 minutes (3s codemod + AI/human review)
Mechanical pattern rewrites100% manual~80–95% automated
Tx-dict → kwargs (deploy / method calls)Find-replace per fileOne pass, zero FP
network.show_active() in subscripts/f-stringsHand-edit eachAuto-rewritten
Exception class renamesLook up Ape docs per nameVirtualMachineErrorContractLogicError automatic
Risk of regressionsMedium (typos, missed sites)Zero (validated on 5 OSS repos incl. Yearn Finance)
brownie-config.yamlape-config.yamlManual rewritescripts/migrate_config.py handles known fields

What it migrates

#PatternBefore (Brownie)After (Ape)
1Module import renamefrom brownie import networkfrom ape import networks
2Contract artifact importfrom brownie import FundMe# TODO(brownie-to-ape): … FundMe
3Built-in unsupported namesfrom brownie import exceptions, WeiTODO comment, dropped from import
4Bare module importimport brownie (when brownie.reverts/.accounts used)import ape
5Reverts / accounts / project / config / chainbrownie.reverts(...)ape.reverts(...)
6Active network namebrownie.network.show_active() and bare network.show_active()networks.active_provider.network.name
7Tx-dict → kwargsContract.deploy(arg, {"from": x, "value": v})Contract.deploy(arg, sender=x, value=v)
8Tx-dict with trailing kwargdeploy(addr, {"from": x}, publish_source=...)deploy(addr, sender=x, publish_source=...)
9chain.mine(N) positional argchain.mine(10)chain.mine(num_blocks=10)
10chain.sleep(N) as a statementchain.sleep(60)chain.pending_timestamp += 60
11Brownie exception class namesexceptions.VirtualMachineError, brownie.exceptions.VirtualMachineErrorexceptions.ContractLogicError, ape.exceptions.ContractLogicError
12accounts.add(pk) inline TODOaccounts.add(pk)accounts.add(pk) # TODO: Ape uses accounts.import_account_from_private_key(...)
13Brownie's isolate(fn_isolation): pass fixturedef isolate(fn_isolation): passTODO comment above the decorator (Ape has chain.isolate() built-in)
14Wei("X") calls (auto-rewrite!)Wei("1 ether")convert("1 ether", int) + auto-injects from ape.utils import convert
15interface.X(addr) callsinterface.IERC20(addr)interface.IERC20(addr) # TODO: Ape's Contract(addr) with explicit ABI/type
16accounts.at(addr, force=True)accounts.at(WHALE, force=True)accounts.impersonate_account(WHALE) (strict force-keyword guard)
17tx.events[N][key] (event index access)tx.events[0]["amount"]tx.events[0].event_arguments["amount"]
18tx.events["Name"][key] (event name access)tx.events["Transfer"]["to"][log for log in tx.events if log.event_name == "Transfer"][0].event_arguments["to"]
19<Contract>[-1] / len(<C>) / <C>.at(addr)Token[-1], Token.at(addr)project.Token.deployments[-1], project.Token.at(addr)
20web3.eth.get_balance(X)web3.eth.get_balance(addr)chain.get_balance(addr) + auto-injects from ape import chain
21ZERO_ADDRESS importfrom brownie import ZERO_ADDRESSfrom ape.utils import ZERO_ADDRESS (moved to ape.utils)
22Unknown exceptions.X referencesexceptions.SomeUnknownExcTODO at top of file listing unmapped exception names
BonusYAML config helper (scripts/migrate_config.py)brownie-config.yamlape-config.yaml (networks, solidity, dependencies translated; legacy file preserved)

Install & Run

bash

Validated on real OSS repos

Tested on five Brownie OSS projects covering different shapes (token tutorial, oracle deploy, multi-network lottery with VRF mocks, Aave DeFi integration, Yearn Finance strategy template):

RepoFiles modifiedPatterns auto-migratedFalse positives
brownie-mix/token-mix4 / 5 .py~62 (3 imports, 30+ tx-dicts, 6 reverts, 2 bare imports, 1 isolate fixture)0
PatrickAlphaC/brownie_fund_me5 / 6 .py~21 (5 imports, 8 tx-dicts, 8 show_active, 1 exception rename, 1 accounts.add TODO)0
PatrickAlphaC/smartcontract-lottery5 / 7 .py~30 (5 imports, 13 tx-dicts, 9 show_active, 1 exception rename)0
PatrickAlphaC/aave_brownie_py_freecode4 / 5 .py~24 (3 imports, 8 tx-dicts, 7 show_active, 5 interface TODOs)0
yearn/brownie-strategy-mix4 / 7 .py~33 (3 imports incl. web3 preserve, 8 tx-dicts, 5 show_active, 4 contract drops + auto-add project)0

Combined: 22/30 files modified across 5 OSS repos. ~170 patterns auto-migrated. 0 false positives.

See CASE_STUDY.md for the full write-up, DEMO.md for curated before/after examples, API_REFERENCE.md for the comprehensive Brownie→Ape pattern map, and benchmark/results.md for timed runs across all five repos.

Zero-False-Positive Guards

The codemod is engineered to never make incorrect changes. Key guards:

  1. File-level marker — transforms only run on files that contain the substring brownie. Files with unrelated {"from": x} patterns (email APIs, regular dicts) are untouched.
  2. Tx-dict whitelist — a dict literal is treated as a Brownie tx-dict only if every key is in {"from", "value", "gas", "gas_limit", "gas_price", "max_fee", "priority_fee", "nonce", "required_confs", "allow_revert"} AND "from" is present.
  3. Contract-name heuristic — uppercase names that aren't built-in Brownie module names (accounts, network, chain, config, project) are assumed to be contract artifacts and dropped from imports with a TODO comment.
  4. brownie.network not auto-renamed — only the specific brownie.network.show_active() pattern is rewritten (to the specific Ape equivalent). Other brownie.network.* is left for manual review since Ape exposes them differently.
  5. Replace dict node, not arg list — preserves edits to surrounding positional args (e.g. brownie.accounts[0] inside the same call gets renamed independently).
  6. Wildcard from brownie import * skipped — too risky to rewrite without symbol tracking.

What's NOT auto-migrated (intentionally)

These patterns are flagged with # TODO(brownie-to-ape): … for manual review or AI-assisted follow-up. They are by design left manual to keep FP at zero.

  • Contract artifacts (Token, FundMe, etc.) — Ape uses project.<ContractName> access. The codemod can't infer the project structure.
  • MockV3Aggregator[-1] style — Brownie's "last deployed" subscript. Ape uses project.<Name>.deployments[-1].
  • accounts.add(private_key) — Ape requires accounts.import_account_from_private_key(alias, passphrase, key).
  • chain.sleep(N) in expressions — Only the statement form is auto-migrated. result = chain.sleep(N) (rare — Brownie returns None) is left alone since the rewrite would change semantics.
  • chain.mine(N, timedelta) — Brownie's two-arg form. Skipped (only single positional N is migrated to num_blocks=N).
  • brownie.exceptions.VirtualMachineError — class names differ in ape.exceptions.
  • brownie-config.yamlape-config.yaml — YAML config schema migration is out of jssg scope; needs a separate transform.

Project Layout

text

Development

bash

Test suite (250 active tests, 0 failures)

SuiteTestsToolPurpose
jssg fixtures90Codemod CLIfull transform snapshot tests (incl. 6 new edge-case fixtures: deeply nested attrs, multi-line imports, comment-preserving tx-dicts, FP guards)
Vitest unit50Vitestpure helpers in isolation
Vitest property11 active + 6 gatedVitestidempotency, determinism
Vitest QA53Vitestversion, docs, perf budget, golden-master
Python pytest29pytestYAML config translator (Describe* + Test*)
Python Hypothesis6Hypothesis + pytestproperty-based fuzzer on YAML translator (idempotency, determinism, serializability)
Plus: real-repo CI verificationGitHub Actionsape-verify.yml runs codemod inside Docker on freshly-cloned repos, see APE_VERIFY_REPORT.md

Rollback

If a codemod run produces something unexpected, every change is in your
target repo's working tree:

bash

The codemod never touches files outside --target and never overwrites
untracked files (the YAML helper renames legacy → .legacy).

Demo cast

A pre-recorded asciinema cast at demo/demo.cast
(asciicast v2, 44 events, ~14s). Play locally:

bash

FAQ

Q: Will running this break my codebase?
A: No. Every change is in your working tree until you git commit. If
the diff looks wrong, run git checkout -- '*.py' to discard. The
codemod is engineered for zero false positives — validated on 4 OSS
repos.

Q: I don't trust it. Can I preview first?
A: Yes. bash scripts/preview.sh /path/to/your/project runs in dry-run
mode and prints a per-file edit summary without modifying anything.
Or use --dry-run directly with codemod workflow run.

Q: My project uses Brownie + web3.py heavily. Will this migrate
web3.py too?

A: Partially. Web3.toWei(...) and Web3.fromWei(...) get inline
TODO comments pointing to Ape's convert(...). Other web3.py patterns
(web3.eth.X) are untouched — they're a different framework upgrade
(web3.py v6 → v7)
that warrants its own codemod.

Q: How long does manual cleanup take after the codemod?
A: Most projects: 5–30 minutes. The remaining work is ~5–20% of the
migration: replace contract artifact references with project.<Name>,
configure accounts via ape accounts import, run ape compile and
fix any compile errors. The codemod's TODO comments mark every spot
that needs attention.

Q: Do I have to use the bundled YAML config converter?
A: Optional but recommended. python scripts/migrate_config.py .
translates brownie-config.yaml to ape-config.yaml for known
fields. If you'd rather convert the YAML manually, just don't run it
— the codemod doesn't depend on it.

Q: Why is my first run so slow?
A: npx downloads the Codemod CLI on first invocation (~10–20s).
Subsequent runs are ~3 seconds. Install once with npm i -g codemod
to skip this.

Q: I have feature X (Curve / Yearn / etc.) in my Brownie repo. Will
it work?

A: The codemod only transforms Brownie SDK patterns — it doesn't
touch protocol-specific code. Validated repos already cover token
contracts, fund-me oracles, lottery + VRF, and Aave DeFi integration.
If you hit a Brownie pattern that isn't migrated, file a feature
request.

Troubleshooting

Symptom: codemod: command not found

  • Use npx codemod@latest … (no global install needed).
  • Or install once: npm i -g codemod.

Symptom: codemod runs but no files change.

  • The target may not be a Brownie project — only files containing the
    substring brownie are processed.
  • Run bash scripts/preview.sh <target> to see whether anything is
    detected.

Symptom: from ape import … line is missing some name.

  • The codemod intentionally drops names that have no direct Ape
    equivalent (contract artifacts, Wei, interface, etc.). Look for
    # TODO(brownie-to-ape): comments above the rewritten import.

Symptom: ape compile fails with NameError: contract not defined.

  • Brownie auto-injects contract artifacts into every namespace; Ape
    doesn't. Replace MyContract.deploy(...) with
    project.MyContract.deploy(...). The codemod's TODO comment marks
    these.

Symptom: pytest fails with

text
.

  • The codemod maps VirtualMachineErrorContractLogicError and a
    few others, but unknown exception names need manual lookup. Check
    the Ape exceptions docs.

Symptom: Output has duplicate from ape.utils import convert.

  • Bug — file an issue. The dedup check (Pass 9) should catch this.

Symptom: Codemod CLI hangs in CI.

  • Pass --no-interactive --allow-dirty flags. The CLI prompts for
    confirmation by default if the target isn't a clean git tree.

License

MIT — see codemod.yaml.

Author

Pugar Huda Mantoro · pugarhudam@gmail.com

Ready to contribute?

Build your own codemod and share it with the community.