How Pyrefly Implements Callable Overload Resolution

Pyrefly resolves callable overloads by storing multiple signatures in a TspOverloadedType and iterating through them at call sites to find the most specific match according to PEP 484 rules, emitting a "No matching overload" error when no candidate succeeds.

The facebook/pyrefly type checker implements Python's @overload decorator system through a multi-stage pipeline that converts stub definitions into an efficient intermediate representation. When performing callable overload resolution, Pyrefly unifies argument types against stored signatures and applies subtype-based selection criteria to determine the correct return type. This architecture ensures spec-compliant behavior while maintaining the performance characteristics required for large-scale Python codebases.

Parsing and Representing Overloaded Types

From @overload to Overload Objects

When Pyrefly parses a stub file containing @overload decorators, each overload block becomes a pyrefly_types::types::Overload struct. This object stores a sequence of signatures that may be plain functions or forall-quantified polymorphic signatures.

Conversion to TSP OverloadedType

The transformation from internal representation to solver-facing type occurs in pyrefly/lib/tsp/type_conversion.rs (lines 56-67) via the convert_overload_to_tsp function. This routine walks the signatures vector, converts each entry to a TspFunctionType, and wraps the results in a TspOverloadedType:

// Convert a pyrefly `Overload` to a TSP `OverloadedType`.
fn convert_overload_to_tsp(
    &self,
    overload: &pyrefly_types::types::Overload,
    bound_to_type: Option<Box<TspType>>,
) -> TspType {
    // (shared bound type handling omitted for brevity)
    let overloads: Vec<TspType> = overload
        .signatures
        .iter()
        .map(|sig| {
            let bt = shared.as_ref().map(|arc| Box::new(TspType::clone(arc)));
            match sig {
                pyrefly_types::types::OverloadType::Function(f) => {
                    self.convert_function(&f.signature, &f.metadata.kind, bt)
                }
                pyrefly_types::types::OverloadType::Forall(f) => {
                    self.convert_function(&f.body.signature, &f.body.metadata.kind, bt)
                }
            }
        })
        .collect();
    TspType::Overloaded(TspOverloadedType {
        flags: TypeFlags::CALLABLE,
        id: next_id(),
        implementation: None,
        kind: TypeKind::Overloaded,
        overloads,
        type_alias_info: None,
    })
}

Callable Overload Resolution at Call Sites

The Resolution Algorithm

When the solver encounters a call expression where the callee has type TspOverloadedType, it iterates over the stored overloads vector. For each signature, Pyrefly attempts to unify the provided argument types against the parameter types, accounting for default values, *args, **kwargs, and bound-method receivers.

The core logic resides in pyrefly/lib/solver/call.rs:

for overload in &overloaded.overloads {
    if args_match_signature(args, overload.parameters) {
        viable.push(overload);
    }
}
// Apply most‑specific selection, emit error if viable.is_empty()

If multiple overloads match, Pyrefly applies the PEP 484 "most specific" rule: the overload whose parameter types are subtypes of all other matching candidates wins. If no unique best overload exists, or if the viable vector remains empty, the solver reports "No matching overload".

Handling Bound Methods

For method overloads, Pyrefly passes the receiver type as bound_to_type to convert_overload_to_tsp. This type is cloned into each overload signature, allowing the solver to treat the method as a callable with an implicit first argument (self or cls) already bound.

Spec-Compliant Mode

Pyrefly supports strict PEP 484 semantics through the spec_compliant_overloads flag defined in pyrefly/lib/test/util.rs (lines 306-307). When enabled, the type checker rejects ambiguous overload sets and enforces exact compliance with the Python typing specification.

Practical Example: Overload Resolution in Action


# example.py

from typing import overload

@overload
def greet(name: str) -> str: ...
@overload
def greet(name: int) -> int: ...

def greet(name):
    # implementation omitted

    ...

# Pyrefly type‑checking

reveal_type(greet("Alice"))   # -> str (first overload selected)

reveal_type(greet(42))        # -> int (second overload selected)

# Incorrect call – no overload matches

reveal_type(greet(3.14))      # ❌ error: No matching overload

Summary

  • Pyrefly represents @overload definitions as Overload objects that get converted to TspOverloadedType via convert_overload_to_tsp in pyrefly/lib/tsp/type_conversion.rs
  • Call-site resolution iterates through stored signatures in pyrefly/lib/solver/call.rs, unifying arguments and tracking viable candidates
  • Selection follows PEP 484 "most specific" subtype rules when multiple overloads match
  • Bound methods handle implicit self/cls arguments through the bound_to_type parameter
  • Strict compliance mode ensures adherence to Python typing spec requirements

Frequently Asked Questions

How does Pyrefly store multiple overload signatures?

Pyrefly stores them in a TspOverloadedType struct containing a vector of TspType objects, where each element represents one @overload signature. This structure is created by the convert_overload_to_tsp function in pyrefly/lib/tsp/type_conversion.rs (lines 56-67), which walks the signatures vector of the source Overload object and converts each entry to a TspFunctionType.

What happens when no overload matches the call arguments?

When no viable overload is found during callable overload resolution, Pyrefly emits the diagnostic error "No matching overload". This occurs in the solver logic within pyrefly/lib/solver/call.rs after iterating through all available signatures and finding that none successfully unify with the provided argument types.

How does Pyrefly handle bound methods with overloads?

For bound methods, Pyrefly passes the receiver type as bound_to_type to convert_overload_to_tsp, which clones this type into each overload signature. This allows the solver to treat the method as a callable where the first argument (self/cls) is already bound, ensuring correct resolution when the method is accessed on an instance or class.

Can Pyrefly enforce strict PEP 484 overload semantics?

Yes, Pyrefly provides a spec_compliant_overloads configuration flag defined in pyrefly/lib/test/util.rs (lines 306-307). When enabled, the type checker enforces strict adherence to PEP 484 rules for overload resolution, rejecting ambiguous overload sets and ensuring compatibility with the official Python typing specification.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →