Migrating from Python
You don’t need to convert your whole codebase. Typhon and Python interoperate; you can migrate a single file at a time. This recipe walks the process.
Prerequisites
- A typed Python codebase. Untyped Python migrates too but with more
unsafe:blocks. tycinstalled; project scaffolded withtyc init(or set up by hand).
Strategy
Migrate leaves first (small utility modules with no inbound deps) → up through the dependency graph. At each step:
-
Run
tyc migrateon the candidate file. Inspect the diff with--check.Terminal window tyc migrate --check src/utils.pytyc migrate src/utils.py # writes src/utils.ty -
Delete or rename
utils.py. (Keep it briefly during transition if other Python modules import from it; Typhon emitsbuild/utils.py, which they’ll pick up.) -
Run
tyc check src/. Fix the diagnostics. Common ones:tyc::missing_binding_kind— addlet(default) ormut(if rebound).tyc::nullable_use— narrow before use.tyc::missing_annotation— annotate.
-
Run the test suite. It exercises the emitted
.py; if tests pass against the migrated.ty, you’re good. -
Commit and move on. Pick the next leaf.
What tyc migrate handles
Automatic rewrites:
Optional[T]/T | None→T?from typing import TypeVar,T = TypeVar("T")→ PEP 695 generic syntax@dataclassdecorators and their imports → dropped (default emit is@dataclass(slots=True))- Module-level annotated assigns (
x: int = 1) →let x: int = 1
What you do by hand
-
Add
let/mutto function-local bindings. The migrator insertsletfor module-level assigns; locals are flagged by the checker (tyc::missing_binding_kind). Runtyc check, scan the diagnostics, decideletvsmut. -
Convert
try/excepttoResult[T, E]where errors are expected. Mechanical translation:# Pythondef parse(raw: str) -> int:try:return int(raw)except ValueError:raise BadInput(raw)# Typhondef parse(raw: str) -> Result[int, BadInput]:try:return Ok(int(raw))except ValueError:return Err(BadInput(raw=raw)) -
Convert
class X(BaseModel)tomodel X:by hand. Auto-detection would mis-fire on cases that need Pydantic-specific decorators (@validator,Field(...)); leave it to the human. -
Mark framework bases
class!.class X(torch.nn.Module):→class! X(torch.nn.Module):. See Class Escape Hatches. -
Sealed-union refactors. Where you have an open union (
A | B), consider adding atype X = A | Balias to seal it and enable exhaustivematch.
Mixed .ty / .py
Plain .py files in src/ are copied to build/ unchanged. They can import from emitted modules, and vice versa. You don’t need to migrate all-or-nothing.
A typical pattern:
src/├── core.ty # migrated├── api.ty # migrated├── workers.py # not yet migrated├── legacy_etl.py # never migrated└── third_party_glue.pyImports cross the boundary fine. Untyped imports from .py are treated as crossing an unsafe: boundary; write .dty stubs if you need them typed.
Common gotchas
let everywhere makes loops fail
The migrator inserts let by default for module-level assigns; locals fail with tyc::missing_binding_kind and the checker doesn’t know what to suggest. For loop counters / accumulators, you need mut. Manually pass through the file.
try/except for control flow
If the Python codebase uses exceptions for expected failures (KeyError to detect missing config, etc.), refactor to Result[T, E] or use dict.get(...). The mechanical port would compile but keep the bad pattern.
Pydantic auto-generated fields
If a Pydantic model uses model_config = ConfigDict(...) or Field(...) with side effects, the auto-converted model may not behave the same. Inspect carefully.
Framework base classes
class X(SomeFrameworkBase): may need class! for the synthesised super().__init__() call. See Class Escape Hatches.
When to stop
You don’t have to migrate everything. Migrating leaf modules first gets the high-value safety wins (typed errors, sealed unions, non-null checks) without the cost of touching glue layers. If a module would mostly be unsafe: blocks, leave it as .py.
Where next
tyc migrate— the command reference.- Calling Python from Typhon — mixed-language interop.
- Class Escape Hatches — framework base patterns.