{"slug": "fwpython-part-1-setting-the-stage", "title": "FwPython, Part 1: Setting the Stage", "summary": "A Pyrefly developer introduced FwPython, a small formal language modeled in Lean 4, to explore edge cases in Python's type system. The language includes classes, inheritance, method lookup, and isinstance tests, but omits modules, mutation, and generics. The project aims to put controlled pressure on typing rules that type checker authors must resolve.", "body_md": "# FwPython, Part 1: Setting the Stage\n\n*Companion post to my PyCon 2026 Typing Summit talk, \"From Soundness to Blame: AI-Assisted Formalization of a Tiny Python.\" This is the first in a series.*\n\n## What this series is about\n\nPython's type system is practical, useful, but full of edge cases.\n\nAs a developer for Pyrefly, I frequently run into questions where the right behavior is not fully settled by the typing spec:\n\n- How should narrowing behave in a union? What's the right interaction between\n`isinstance`\n\nand`Any`\n\n? - When two callable intersections meet an\n`Any`\n\nargument, what comes back? - The conformance tests cover some of this; the spec covers some of this; past that, type checker authors still have to make choices.\n\nThose choices are usually local when you first encounter them. A rule looks reasonable in one example, questionable in another, and subtly different from what another checker does. I wanted a way to take some of those local rules and push them through the entire language: how do they interact with, e.g. method lookup, subtyping, narrowing, and calls? Do they still behave sensibly when other rules have to agree with them? Do they break something that previously seemed settled, or does it expose a real tradeoff?\n\nFwPython is a small language for doing that experiment.\n\nIt is a compact object-oriented language formalized in [Lean 4](https://lean-lang.org/), a theorem prover and functional programming language. It is Python-like enough to talk about classes, inheritance, method lookup, first-class bound methods, callables, and branch-sensitive `isinstance`\n\ntests, but small enough that the runtime, type checker, and the claims connecting them can all be checked by one machine.\n\nThat makes FwPython useful as a model, but also defines its limits.\n\n- It keeps nominal classes, multiple inheritance through\n[C3 linearization](https://www.python.org/download/releases/2.3/mro/), runtime method lookup, first-class bound methods, strict boolean conditionals, and branch-sensitive`isinstance`\n\ntests. - It leaves out modules, top-level functions, fields, mutation, constructors, descriptors, metaclasses, decorators, async, Python's full argument-binding rules, generics, etc.\n- A proof about FwPython is not automatically a proof about Python. The point is to put controlled pressure on the parts of Python typing that Pyrefly and other checkers actually have to reason about.\n\nThis first post sets up the source language and the runtime model. Later posts will use that setup to discuss the type system, soundness, gradual typing, and more.\n\n## The components\n\nThe Lean development has five connected pieces:\n\n**Source language**: the syntax of FwPython programs.** Interpreter**: the small-step runtime semantics.** Type checker**: the static rules for accepted programs.** Theorems**: the formal claims relating typing to runtime behavior.** Proofs**: the machine-checked arguments for those claims.\n\nLean matters here because definitions and proofs live in the same environment. The interpreter, type system, and theorem statements are ordinary Lean definitions; the proof terms are checked by Lean's kernel. For this post, the important consequence is that a claim about FwPython has to line up with the actual definitions. The runtime described below is the runtime the later proofs use.\n\n## The language\n\nA FwPython program has two components: a *class table* and a *main expression*. There are no top-level statements, no module system, and no separate function definitions. Methods live inside classes; classes live in the class table; the main expression is what gets evaluated when the program runs.\n\nHere is the running example for this post. It uses Python-like notation for readability. I write `Callable[[Object], Object]`\n\nas blog notation for FwPython's unary callable type: an object that can be called with an `Object`\n\nand returns an `Object`\n\n.\n\n``` php\nclass Animal:    def speak(self, x: Object) -> Object:        return xclass Dog(Animal):    def speak(self, x: Object) -> Object:        return Trueclass SpeakerBox:    def get_speaker(self, x: Dog) -> Callable[[Object], Object]:        return x.speak# main expression((new SpeakerBox).get_speaker(new Dog))(None)\n```\n\nThe main expression allocates a `SpeakerBox`\n\n, allocates a `Dog`\n\n, asks the box for that dog's `speak`\n\nmethod, and then calls the returned method value on `None`\n\n. The lookup for `speak`\n\nis ordinary dynamic dispatch through the dog's MRO; the extra point is that the selected method can leave the original expression as a first-class value. The whole program evaluates to `True`\n\n.\n\nThis example is small, but it touches several features FwPython is meant to isolate: nominal inheritance, runtime method lookup, first-class bound methods, and a call through a method value.\n\n### Classes and methods\n\nA class declaration carries three pieces of information:\n\n- the class name\n- a list of direct base classes\n- a list of methods\n\nA method declaration carries six pieces:\n\n- the method name\n- the receiver parameter name, like\n`self`\n\n- one ordinary parameter name\n- the parameter's type annotation\n- the return type annotation\n- the body expression\n\nThis is intentionally a small language. Classes have methods but no fields, so the core calculus can focus on dispatch, callability, subtyping, and narrowing without threading mutation through every invariant. Read-only field-like behavior can be encoded with a method that ignores its argument and returns a constant.\n\nMethods are unary: one explicit argument in addition to the receiver. That is enough to exercise method lookup and callable variance while avoiding Python's positional, keyword, default, and variadic argument machinery. Those details matter for a production checker, but they would obscure the particular metatheory this project is trying to study.\n\nIf a class declares no explicit base classes, the runtime treats it as implicitly inheriting from `Object`\n\n, unless the class itself is `Object`\n\n. Multiple inheritance is supported through C3 linearization, the same MRO algorithm Python uses. In the running example, `Dog`\n\n's MRO starts with `Dog`\n\n, then `Animal`\n\n, then `Object`\n\n, so `x.speak`\n\nresolves to `Dog.speak`\n\nbefore `Animal.speak`\n\n.\n\n### Expressions\n\nFwPython has nine expression forms.\n\n| Form | Reading | What it does |\n|---|---|---|\n`x` | variable | Look up `x` in the runtime environment. |\n`new C` | constructor | Allocate a fresh object of class `C` . No `__init__` runs. |\n`e.m` | attribute access | Resolve method `m` through the MRO of `e` 's class, allocate a fresh bound-method object, return its reference. |\n`e1(e2)` | call | Invoke `e1` on `e2` . Succeeds only when `e1` is a bound-method object. |\n`None` | constant | The distinguished `None` object. |\n`True` | constant | The distinguished `True` boolean object. |\n`False` | constant | The distinguished `False` boolean object. |\n`if e1 then e2 else e3` | conditional | Evaluate `e1` ; on `True` continue with `e2` , on `False` continue with `e3` . |\n`isinstance(x, C)` | type test | Test whether the runtime class of the value bound to variable `x` lies under class `C` in its MRO. |\n\nFour choices are worth calling out.\n\n`new C`\n\nallocates an ordinary object and does not invoke a constructor. FwPython is studying object identity, dispatch, and method values, so initialization order and missing-field reasoning are outside this model. The evaluator also rejects `new Bool`\n\n, `new NoneType`\n\n, and `new BoundMethod`\n\n; those built-ins have controlled allocation paths in the runtime.\n\n`isinstance(x, C)`\n\ntakes a variable name as its first argument. That restriction exists because `isinstance`\n\nis also a narrowing construct. After the test, the type system can refine the environment entry for `x`\n\n; allowing an arbitrary expression would require a separate account of what value is being tracked across the branches.\n\nConditionals use exact booleans. A condition must evaluate to the fixed `True`\n\nobject or the fixed `False`\n\nobject. Python's truthiness protocol is rich and useful, but modeling it would add another dispatch mechanism before the series even reaches the typing questions it is trying to expose.\n\nAttribute access is the heavyweight expression form. Evaluating `e.m`\n\nmeans evaluating `e`\n\n, looking up `m`\n\nthrough the runtime class's MRO, allocating a new heap object that stores the receiver and selected method declaration, and returning a reference to that heap object. This mirrors the Python behavior that makes `obj.method`\n\na value you can pass around and call later.\n\n## The runtime\n\nThe interpreter is deliberately boring at the value level. A runtime value has one shape:\n\n```\nobjRef oid\n```\n\nLooking up `oid`\n\nin the heap recovers the information that matters operationally:\n\n- a runtime class name\n- a payload, either ordinary or bound-method\n\nOrdinary payloads carry no extra data. Bound-method payloads carry the receiver object id and the method declaration chosen by lookup. The initial heap contains three ordinary objects at fixed ids: `None`\n\nat `0`\n\n, `True`\n\nat `1`\n\n, and `False`\n\nat `2`\n\n.\n\nFwPython's type system needs to talk about several views of the same runtime value: \"this object has class `C`\n\n\", \"this object is callable with parameter type `P`\n\nand return type `R`\n\n\", and \"this object's runtime class is not under `C`\n\n\". All three are evidence about one heap object rather than separate value constructors.\n\n### Why bound methods are objects\n\nPython has many callable carriers: functions, lambdas, bound methods, descriptors, built-in functions, callable instances, and class objects. FwPython collapses that space to one producer of callable values: method binding. Attribute access resolves a method through the receiver's MRO, allocates a `BoundMethod`\n\nheap object, and returns its object reference.\n\nIn the running example, evaluating `x.speak`\n\nin the body of `get_speaker`\n\nperforms three runtime steps:\n\n- look up the object currently bound to\n`x`\n\n; - compute that object's MRO and find\n`speak`\n\n; - allocate a\n`BoundMethod`\n\nobject whose payload stores the receiver id and the selected`Dog.speak`\n\ndeclaration.\n\nThe final expression then calls the object returned by `get_speaker`\n\n. The call rule inspects the callee's heap entry. If the payload is bound-method data, the machine evaluates the argument, runs the stored method body with the receiver and parameter bound, and restores the caller environment afterward. If the callee is an ordinary object, the machine reports a runtime error.\n\nThe collapse to one callable carrier is a modeling choice to keep the formal story tied to method lookup plus first-class method values. A method value can be returned, stored in the environment, passed as an argument, and invoked later, and the runtime handles all of those cases with the heap object created by attribute access.\n\nThis also makes the later typing rules simpler to state. If the type checker says that a value has type `callable P R`\n\n, the runtime fact behind that claim is always the same: the value points to a `BoundMethod`\n\nobject whose payload contains a receiver and a method declaration. The checker can then validate the callable type by checking the stored method's parameter and return types, and by checking if the method really came from a lookup on the receiver's class.\n\nBut there's a trade-off. FwPython skips over things like user-defined `__call__`\n\n, stand-alone functions, Class-object-as-callable, or full-blown descriptors. Sure, these are real parts of Python, but adding them would just add more \"callable carriers\" to track and significantly increase the proof workload. Instead, I've decided to spend that complexity budget focusing on method dispatch, bound methods, and the typing rules that govern them.\n\n### Why the built-ins are ordinary heap objects\n\nFour distinguished classes, `Object`\n\n, `Bool`\n\n, `NoneType`\n\n, and `BoundMethod`\n\n, live in the same class table as user-defined classes. Most machinery sees them as ordinary class entries, with a small number of runtime and typing rules giving them fixed meaning:\n\n`Object`\n\nis the nominal root of the hierarchy. Every class transitively inherits from`Object`\n\n.`Bool`\n\nis the runtime class of`True`\n\nand`False`\n\n.`NoneType`\n\nis the runtime class of`None`\n\n.`BoundMethod`\n\nis the runtime class of every bound-method object.\n\nThe constants are ordinary heap objects because the runtime has no special boolean or none value form. Conditionals distinguish truth from falsity by the two fixed object ids. `isinstance`\n\ntreats `None`\n\n, `True`\n\n, and `False`\n\nby the same MRO test it uses for user-created objects.\n\nThe evaluator also refuses `new Bool`\n\n, `new NoneType`\n\n, and `new BoundMethod`\n\n. That restriction preserves the heap invariants the proofs rely on: the only boolean objects are the two fixed constants, the distinguished `None`\n\nobject stays at its fixed id, and bound-method objects are introduced by method lookup with a receiver and method payload already attached. In the class table, `Bool`\n\n, `NoneType`\n\n, and `BoundMethod`\n\nare leaf built-ins, which is what lets the later negative-type rules make open-world-safe claims about them.\n\n## Where this leaves us\n\nSo far we have the source language and the runtime model. Later posts will build on this setup to discuss the type system:\n\n- if narrowing for\n`isinstance(x, C)`\n\n- negation type and its restrictions\n- subtyping rules\n- how we could formally extend a fully static type system with gradual types like\n`Any`\n\nThose topics are the reason for the small language above. The runtime carries just enough evidence for the type system to make precise claims, and no more than the later proofs are prepared to justify.\n\nThe project is open source at [github.com/grievejia/FwPython](https://github.com/grievejia/FwPython).", "url": "https://wpnews.pro/news/fwpython-part-1-setting-the-stage", "canonical_source": "https://pyrefly.org/blog/fwpython-part-1/", "published_at": "2026-07-10 00:00:00+00:00", "updated_at": "2026-07-14 15:20:02.483248+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "ai-research", "ai-tools", "developer-tools"], "entities": ["Pyrefly", "Lean 4", "Python", "PyCon 2026"], "alternates": {"html": "https://wpnews.pro/news/fwpython-part-1-setting-the-stage", "markdown": "https://wpnews.pro/news/fwpython-part-1-setting-the-stage.md", "text": "https://wpnews.pro/news/fwpython-part-1-setting-the-stage.txt", "jsonld": "https://wpnews.pro/news/fwpython-part-1-setting-the-stage.jsonld"}}