Skip to content

Imports

Imports follow Python’s syntax, with one Typhon addition (lazy import) and one target-dependent form (lazy from, which needs a 3.15 target).

import

import sys
import os.path
import json
import requests as rq

Standard Python — names land in the importing module’s namespace.

from ... import

from typing import Callable
from collections.abc import Iterable
from .users import User
from ..lib import helper

Relative imports (., ..) work as in Python. Since v0.9.0 the in-process VM (tyc run) also resolves relative imports against the project source root — sibling .ty modules load on demand and each module’s bindings are cached as a Value::Module. Multi-file projects no longer require --compile to handle the cross-module imports.

tyc run --compile was tweaked at the same time: instead of python build/main.py, it spawns python -m <pkg>.main so relative imports in the entry point resolve correctly under the compiled path too.

typing names

Inside annotations and type statements — which the emitted module never evaluates — Union, Optional, Any, List / Dict / Set / Tuple / FrozenSet, Type, TypeVar, Generic, Self, ClassVar, Final, Literal and NoReturn can be written without an import. Used as runtime values they need one, because CPython would raise NameError:

def describe(hint: object) -> str:
return "union" if hint is Union else "other" # ❌ tyc::unknown_name
from typing import Union
def describe(hint: object) -> str:
return "union" if hint is Union else "other" # ✅

Protocol and the collections.abc names (Callable, Iterator, …) are unaffected: the build imports them. The deprecated capitalised aliases and TypeVar cannot be imported at all (tyc::typing_alias_deprecated, tyc::typevar_import_rejected); use the lowercase built-ins and PEP 695 type parameters.

Conditional imports

import sys
if sys.platform == "win32":
import winreg
else:
import os

Standard Python — Typhon doesn’t restrict this.

Re-exports

A module-level __all__ list controls what from module import * exposes:

# in users.ty
__all__ = ["User", "find_user"]

For imports the resolver follows __all__ when computing what a from module import provides.

lazy import

Defers module loading until first attribute access:

lazy import np = numpy
lazy import torch

The first form gives the proxy an alias; the second uses the module name directly. See lazy for the full reference.

lazy from needs Python 3.15

lazy from numpy import array # ✅ on a 3.15 target, ❌ on 3.13 / 3.14
error[tyc::requires_newer_python]: `lazy from … import …` needs Python 3.15 (PEP 810)
and the project targets 3.13; use
`lazy import ALIAS = MODULE` instead

Before PEP 810 a from-import has to load the module to bind the name, so it cannot defer. On a 3.15 target the native statement makes the name itself lazy, and tyc build emits it as written.

Unused imports

Severity controlled by [strictness] unused-import in typhon.toml:

  • "warn" — default since v0.8.0 (was "error" before). Unused imports surface as warnings.
  • "error" — fail the build on unused imports. Set this for CI strictness or to enforce a clean import surface.
  • "off" — no check.

The LSP offers a “Remove unused import” quick-fix.

Import side effects

Python imports run module-level code. Typhon does not change this — import some_module runs some_module/__init__.py as it always has.

Use lazy import to defer that cost when it matters (e.g. for CLIs that may not need the module).

Cyclic imports

Typhon does not introduce new rules. Python’s existing semantics apply: cyclic imports work as long as the cycle doesn’t access module-level names before they’re defined. The checker doesn’t enforce a “no cycles” rule.

Where the checker treats .pyi / .py as boundaries

When you import from a plain .py (no .dty stub authored), the imported symbols are treated as if they crossed an unsafe: boundary unless they’re typed in the source. Use .dty stubs to give third-party libraries proper typed signatures.

Where next