1. The Reality of Migrations
Migrating long-standing commercial or internal Python services is rarely a simple upgrade. Over years of development, codebases accumulate technical debt that spreads across Python runtimes, package manifests, and configuration layouts. Major version hops bring deep behavioral modifications, making upgrades feel unpredictable. Without continuous dependency health, teams defer upgrades, turning technical debt into an open-ended research task.
The Research & Try Loop (Unpredictable)
Engineers manually research package changes, upgrade versions blindly, debug runtime errors post-upgrade, rewrite configuration formats, and repeat the trial-and-error cycle across dozens of legacy systems.
The PyMolt Playbook (Predictable Checklist)
PyMolt maps out code references, runtime versions, and configs beforehand. It generates a full topological roadmap of where, what, and how to change — converting uncertainty into a structured checklist.
2. Phase 1: The Internal Audit
Before modernizing, engineers need a holistic picture of the project's internal structure. Traditional package listing commands only list immediate, direct dependency names. They ignore python version boundaries, setup manifests (such as pyproject.toml, setup.py, or requirements files), and nested transitive dependencies.
PyMolt structures and digests this metadata from the inside out. Upgrading a direct framework like Django can force upgrades on utility modules like urllib3 or asgiref, which might crash legacy environments. PyMolt models these connections upfront to ensure no runtime parameter changes go unseen.
$ pymolt scan --json ./services
{
"roots": 5, "python": { "baseline": "3.8", "diverges": true },
"dependencies": [
{ "name": "pandas", "current": "1.5.3", "target": "2.2.0", "risk": "behavioral" },
{ "name": "numpy", "current": "1.23", "paths": ["direct", "via:pandas"] }
]
}
3. Phase 2: AST Call-Graph Mapping
Upgrading library versions is easy; updating code signatures is hard. Poor test coverage means you can't rely solely on unit tests. If a deprecated function is only executed inside an exceptional handlers class, it will slip into production unnoticed.
PyMolt parses Python source code into an Abstract Syntax Tree (AST). It trace imports, calls, and assignments to trace where deprecated functions are invoked. This call-graph tracking gives engineers total visibility of exactly where upgrades will break.
| Analysis Dimension | Manual / Regex Search | PyMolt AST Scan |
|---|---|---|
| Call Tracing | Fails to distinguish native functions from variables with similar names. | Traces fully-qualified package import namespaces. |
| Transitive Upgrades | Invisible until execution runtime crashes. | Predicts downstream package signature breakages. |
| Security CVEs | Lists vulnerabilities, but cannot tell if unsafe code is actually executed. | Verifies if the specific vulnerable method is called. |
4. Phase 3: Upgrade Strategy & Dependency Hell
When you have dozens of dependencies, you hit version lock. Package `A` requires package `X < 2.0`, while package `B` requires package `X >= 2.1`. Upgrading both requires finding a version coordinate corridor.
PyMolt builds a topological dependency sorting sequence. It identifies blocker nodes (e.g. library dependency locks or deprecated plugins) and isolates them, allowing engineers to resolve blockers before beginning general upgrades.
5. Phase 4: Behavioral Verification — the contract
Resolution says a dependency installs. It says nothing about whether your code still behaves the same. This is the class of change linters and LLM agents miss entirely: the syntax stays valid, the code keeps running, and the behavior quietly shifts. It only surfaces at runtime, on real data.
PyMolt traces every call crossing from your code into a target dependency — under the old and the new version — and diffs the contract. A pure-stdlib runtime is injected into the target interpreter (Python 3.6+), so nothing is added to your codebase. Below are two real behavioral changes from this migration: PyMolt detects them, it does not silently rewrite them.
df["foo"].fillna(0, inplace=True)
df.fillna({"foo": 0}, inplace=True)
# same syntax in v1 returned a different result — a tracer catches it, a linter cannot
$ pymolt contract diff old.jsonl new.jsonl
🔬 Folding two recordings into a BoundaryDiff...
result_changed pandas.DataFrame.fillna → BEHAVIOR_CHANGED
skipped_opaque numpy.ndarray.__repr__ → NEEDS_ACTION
A behavior change is only trustworthy over the area the dynamics covered.
The codemod layer acts on this contract: automation that knows what changed, to what, and why — not blind syntax rewriting that can't tell a behavioral change from a cosmetic one. How that works is the next section.
6. The rewrite engine
Rewrites run through LibCST, a concrete syntax tree for Python. Unlike a plain AST, a CST keeps every token the parser would otherwise discard — comments, blank lines, trailing commas, the exact quote style you used. A rewrite is a tree transformation, so the diff contains only the lines that actually changed. Nothing is reformatted as a side effect, and no regex ever touches your source.
Tier 1 — mechanical. Renames, moved imports, dropped keyword arguments. Derived from the public release diff of a canonical version pair, so any package on PyPI can get one on demand.
Tier 2 — behavioral. Declarative match templates carrying metavariables, hand-authored per library. A template binds against tree shape rather than text, so one rule covers every spelling of the same call:
match: $DF.sum() replace: $DF.sum(numeric_only=True) when: $DF is a pandas.DataFrame why: numeric_only default flipped False in 2.0
Every candidate rule — including one the hub already labelled verified — is re-executed locally against your own golden pair before it is offered. A rule that fails that re-check is downgraded to a heuristic suggestion and never applied silently. PyMolt, not the server, is the verification authority.
Before anything runs, PyMolt enumerates the interpreters that actually exist on the machine rather than trusting a classifier: project .venv directories via pyvenv.cfg, the active virtualenv, Conda environments, pyenv versions, Poetry and Pipenv caches, Docker base images declared in the Dockerfile, and finally the system PATH. Where the project targets a runtime nobody has installed, it derives a Dockerfile.pymolt-baseline or emits the exact install commands — comparing behavior genuinely requires the old runtime, and PyMolt's job is to make getting it cheap rather than to pretend otherwise.
What leaves your machine
a package name and two version stringsThe request carries a package name and two version strings. No source, no file names, no lockfile, no telemetry — and a rule the hub calls verified is still re-run against your code before it is applied.
A recipe request carries a package name and two version strings. That is the entire payload — no source, no file names, no lockfile. Responses land in .pymolt_cache, so a second run, a CI job, or an air-gapped machine replays them with no network at all. Point PYMOLT_ENDPOINT at an internal server and nothing leaves your perimeter in the first place.
Every fact reports where it came from and how confident it is — a Dockerfile ARG chain, a lockfile, a tox matrix. Where a value can't be compared, the report says so: skipped_opaque for values the tracer cannot serialise, BLIND for call sites the test run never reached. A migration is usually run by someone who does not know the codebase, so “unknown” has to be an available answer. Unverified paths are listed, never quietly folded into the pass count.
7. Why PyMolt?
Upgrading dependencies isn't just maintenance; it's security and team velocity. By replacing manual audits and surprise runtime errors with a navigable dependency map, public-source risk scoring, and — the part nothing else does — a behavioral diff at the dependency boundary, PyMolt turns the research project back into a predictable checklist. You get a known scope, an honest trust level, and a clear list of what still needs a human.
Run the whole method yourself
Every phase above is a command in the open-source CLI and a tool your agent can call over MCP — free, no tiers, no limits. Got a migration that's really a research project? That part is still a conversation.