Skip to content

Calling Python from Typhon

When you import from a plain .py, several outcomes are possible:

  1. The module is typed (PEP 484 / 526 annotations everywhere). The checker accepts the types and treats the import like a Typhon module.
  2. An authored .dty stub exists for the module. The stub is the typed surface; the runtime .py is the implementation.
  3. A compiler-bundled stub exists for the module (httpx, requests to start). The library is shaped out of the box — no .venv or tyc sync needed.
  4. The package is installed and introspectable. tyc reads its signatures from the venv and type-checks your calls’ argument types and arity automatically.
  5. The module is untyped. Imports return Any — values bind silently today, but the type system can’t reason about them. Wrap the boundary in unsafe: or annotate the call site to keep the rest of the code base honest.

For the full model — and how these layers compose — see Layers of third-party type-checking.

Typed Python imports

# typed_lib.py — typed Python
def add(a: int, b: int) -> int:
return a + b
# main.ty
from typed_lib import add
def f() -> int:
return add(1, 2) # ✅ types flow through

The checker reads the Python type annotations and treats add like a Typhon function.

Stubbed Python imports

# src/stubs/redis.dty — authored stub
class Redis:
host: str
port: int
impl Redis:
def get(self, key: str) -> str?
def set(self, key: str, value: str) -> bool
# main.ty
import redis
def f() -> str?:
let r: redis.Redis = redis.Redis(host="localhost", port=6379)
return r.get("k") # ✅ typed via the stub

See Writing .dty Stubs for the stub format.

Bundled-stub imports (httpx / requests)

Some libraries are typed via typeshed rather than in their own source, so venv introspection can’t recover their signatures. tyc ships curated, embedded .dty stubs for the most common of these — httpx and requests to start — and seeds them before introspection runs:

import httpx
def fetch() -> httpx.Response:
let client = httpx.Client(timeout=10.0) # ✅ constructor type-checked
return client.get("https://example.com") # ✅ qualified return type

No .venv or tyc sync is required, and the library’s unintrospectable-dependency warning is suppressed. The bundled class shapes are partial, so members the stub doesn’t enumerate stay lenient (no false attribute_not_found). An authored .dty for the same module always overrides the bundle — write one when you want a stricter surface. See Compiler-bundled stubs.

Introspection-checked imports

When a declared dependency is installed and introspectable, tyc reads its signatures straight from the venv via inspect.signature — recovering parameter and return annotations — and type-checks your calls with no authoring at all:

import typed_dep
def f() -> None:
typed_dep.process("not an int") # ❌ tyc::type_mismatch (wants int)
typed_dep.process(1, 2, 3) # ❌ wrong arity

This covers both function and constructor calls to fully-typed pure-Python deps, through the same tyc::type_mismatch machinery your own code uses. It degrades to a permissive Unknown on anything it can’t model, so it only ever adds true positives.

Untyped Python imports

import messy_lib
def f() -> int:
let raw = messy_lib.fetch() # binds to Any — opaque to the checker

Three fixes, in order of preference:

# Option A: write a .dty stub
# (best for long-lived dependencies)
# Option B: annotate explicitly
let raw: dict[str, int] = messy_lib.fetch()
# Option C: wrap in unsafe
unsafe:
let raw = messy_lib.fetch()
let parsed: dict[str, int] = ... # re-assert at the boundary

What gets emitted

Typhon’s import lowers to a plain Python import. The imported name’s runtime behaviour is unchanged:

import messy_lib

The strictness is compile-time only. At runtime, the import resolves the same way Python always has.

Calling async Python

import some_async_lib
async def f() -> str:
let body: str = await some_async_lib.fetch_async("url")
return body

Calling an async def Python function from a Typhon async def works as expected. Sync function calling async without await is a hard error (tyc::missing_await).

Where next