tyc migrate
tyc migrate PATH [--check] [--force]Converts typed Python (.py) to Typhon (.ty) in one pass. Used to bootstrap a Typhon project from an existing typed Python codebase.
What it rewrites
-
Optional[T]/T | None→T?in annotation positions. -
@dataclassdecorators and theirfrom dataclasses import dataclassare dropped (Typhon classes emit as dataclasses by default). -
Module-level annotated assignments (
x: int = 1) gainlet(ormutif reassigned later in the same module). -
Function-body plain assignments (
user = find_user(1),total = 0) gainleton the first occurrence of each name per function. If the same name is reassigned anywhere else in the file (in this function or another), the first occurrence is promoted tomutinstead. Subsequent assignments to a name already bound in the same scope are left bare (a correct re-binding). Class-body annotated assignments are left alone — those are field declarations, not locals.The reassignment flag is file-wide (a single set, not per-function), so a
total = total + 1accumulator in one function will also tag a one-shottotal = 0in an unrelated function asmut. That’s a deliberate over-approximation —mutof an unmutated binding still type-checks, whereas the inverse (leton a counter) would not. If the spuriousmutbothers you, rename the unmutated local. -
from typing import TypeVarplusT = TypeVar("T")scaffolding is dropped; generic functions become PEP 695 form.
v0.13.0 enhancements
The migrator now does more of the idiomatic-Typhon rewrite automatically:
- Class-body methods are relocated into
implblocks. A class with methods is split into the bare field-declarationclassplus a siblingimpl ClassName:block holding the methods. class X(Enum):is rewritten to theenumkeyword. A plainEnumsubclass becomes the first-classenum X:declaration form.field(default_factory=...)is simplified to bare-literal sugar.field(default_factory=list)/field(default_factory=dict)collapse to the= []/= {}mutable-default form Typhon recognises.- Imports those rewrites orphan are pruned — e.g. the now-unused
from enum import Enumorfrom dataclasses import fieldis dropped.
Together these mean migrated output checks with zero errors and zero warnings.
Migrated output is intended to pass tyc check on first try.
What it never touches
Rewrites fire on code only. Text inside a string literal — a docstring’s prose, a doctest, an embedded SQL or HTML template — is copied through verbatim, whatever it happens to look like. A line of docstring that reads like a binding is still a line of docstring.
A method also travels into its impl block whole, however its signature or
docstring is laid out: a parameter list wrapped over several lines, a
docstring whose backslash continuation puts a line at column zero, a stack
of decorators above it.
Where the frozen modifier lands
@dataclass(frozen=True) becomes Typhon’s frozen modifier, which binds
to the class name and sits ahead of any base list:
class Vec frozen(Base): # not `class Vec(Base) frozen:` x: intType parameters stay glued to the name too — class Stack[T] frozen(Base):.
Blocks a rewrite empties
When a rewrite deletes the only statement of a block — a Protocol import
that interface made dead inside a if sys.version_info >= ...: guard, say
— the header would be left with no suite. The migrator puts a pass under
it so the output still parses.
How far it gets
As a scale check: 1,110 of the 1,114 .py files in a CPython 3.13 lib/
tree — the stdlib plus site-packages — migrate to source that parses as
Typhon. The four that do not are heavily metaprogrammed
(typing_extensions, pkg_resources, yaml.resolver). Parsing is not
type-checking: the sequence below is still the way to port a real codebase.
Examples
tyc migrate src/app.py # writes src/app.ty alongsidetyc migrate --check src/app.py # preview to stdout (no writes)tyc migrate --force src/ # re-migrate, overwriting existing .ty filesPATH is a .py file or a directory (migrated recursively). A directory is migrated whole, tests/ included; only directories nobody writes by hand are skipped — virtual environments (any directory holding a pyvenv.cfg, plus .venv, .tox, .nox), VCS metadata (.git, .hg, .svn, .bzr), caches (__pycache__, .mypy_cache, .pytest_cache, …), node_modules and build. If any target .ty already exists, tyc migrate refuses before writing anything and names the file; --force (-f) overwrites instead. A .ty that is a symlink is never written through, even with --force.
--check mode
--check is a preview flag: it prints the migrated source to stdout (one block per file, headed by # ── <path> ──) instead of writing .ty files to disk. It does not compare the output against the original, and it always exits 0 on a successful migration — there is no “no diffs needed” exit code today.
To confirm in CI that a .py file is already Typhon-compatible, pipe the preview into diff against an expected .ty (or run tyc check on the preview output via a tempfile). A native exit-1-on-changes mode is a deliberate follow-up.
What it can not do automatically
- Rewrite
try/exceptintoResult[T, E]. That’s a design-level refactor, not a mechanical translation. Migrate the file first; then refactor errors incrementally. - Convert
class X(BaseModel)intomodel X:. Pydantic-using classes are detected but not auto-converted; you do it manually because the field ordering and Pydantic-specific decorators (@validator,Field(...), etc.) need attention. - Subclasses of framework bases (
torch.nn.Module,Enum,unittest.TestCase) are not detected as candidates forclass!. After migration, convert them by hand.
The migrator is the boring 80%; the remaining 20% is design work.
Recommended migration sequence
- Audit the project. Identify modules that are obvious wins (pure value types, helpers without Pydantic / framework bases) vs ones that need design work.
- Migrate a leaf module first (
tyc migrate src/utils.py). Verify withtyc check. The migrator now insertslet/muton function-body locals, so the typical first run should be clean — any remaining diagnostics are usually design issues (try/exceptpatterns, framework bases) rather than missing keywords. - Migrate adjacent modules. Where you cross into framework bases, mark them
class!(or leave them as.pyfor now). - Convert Pydantic-using classes to
modelby hand when ready. - Refactor
try/excepttoResult[T, E]module-by-module as time allows.
You don’t have to migrate the whole project — .ty and .py interoperate freely. The dial moves at whatever speed is right for your team.
Mixed .ty / .py projects
Plain .py files in src/ are copied to build/ unchanged. They can import from emitted modules, and vice versa. See Project Layout for the conventions.
Where next
- Migrating from Python (recipe) — design patterns and pitfalls.
- Calling Python from Typhon — how interop works across the boundary.