I propose that Python formally allow forward declarations of functions and classes using the same “empty” syntax currently valid within typing.Protocol or stub files (.pyi). Specifically, a definition using the ellipsis literal ... would serve as a promise to the compiler that the full implementation follows later in the same module.
Python’s execution model is strictly top-down. While this is straightforward, it forces a specific organizational “gravity” on code:
Readability: We are often forced to place “leaf” functions at the top of a file and the main entry points at the bottom. This is the opposite of how most people read. We want to see the high-level intent first and the implementation details “way at the bottom.”
Circular Logic: While Python handles late binding for function bodies, certain decorators, type-hint linters, and architectural patterns benefit from the symbol existing in the namespace before the heavy implementation block is defined.
Same Syntax: This proposal requires no syntax changes. The def name(): ... is already valid syntax in Python; it just currently results in a function object that does nothing or a parse-time error.
Proposed Behavior
Declaration: A def or class block containing only ... after : is treated as a forward declaration. A promise that a body with the exact same signature will be provided later, in the same module.
Fullfilment: The compiler/interpreter must ensure that a symbol declared this way is followed by a “concrete” definition within the same module.
Errors: If a forward declaration is made but no implementation is provided by the time the module is executed, the interpreter will raise a TypeError over the missing body.
Namespace: The first declaration reserves the name in the namespace. The second definition updates the existing entry rather than shadowing it, honoring any decorators applied to the first declaration (?).
Comparison with Protocols
We already use this syntax in Protocols:
class Plugin(Protocol):
def run(self) -> None: ...
Extending this to other declarations brings a consistent “interface-first” capability to regular Python modules. It allows developers to “outline” a module at the top as a form of live documentation—and fill in the guts below.
Unknowns
Where do docstrings go?
Do we really want decorators applied on the first declaration?
There are use cases for fulfilling forward declarations in another module, but the bookkeeping is minimal only if the fulfillment happens in the same module.
What to do with @dataclass and other decorators for classes.
I tried using typing.overload, but the semantics are not those of a forward declaration
Should a @forward decorator be required? Would it be useful?
Or, you can simply omit the first declaration. Doesn’t this work?
def first_entry_point(x: Any) -> Any:
# Logic that references second_entry_point
return second_entry_point(x)
def second_entry_point(*args) -> Any:
# Implementation details here
Maybe there are more complex cases. Can you show us an example where you needed the forward declaration? You mentioned “certain decorators, type-hint linters, and architectural patterns” but you gave no examples.
I’m guessing you want to forward declare a decorator…
def some_decorator(f): # I want to write this at the bottom
...
return f
@some_decorator
def foo():
do something
… or maybe a callback?
def some_callback(): # I want to write this at the bottom
do something
thread = threading.Thread(target=some_callback)
Those are the only examples I can think of where you can’t already write functions in any order.
Not sure how forward declarations would make sense in either of those cases though.
A Python module isn’t just a soup of definitions. It’s still a script which is executed sequentially from beginning to end – even if the only execution is defining some functions. Expecting Python to implicitly hop over parts of code it can’t execute yet because it essentially has an undefined variable and then come back later to finish the job is quite a break in how Python executes.
Larry Hastings and I proposed this at a Language Summit years ago. There was no appetite for it. Larry designed PEP 649 to largely solve the typing version of this problem.
def some_callback(): # I want to write this at the bottom do
something thread = threading.Thread(target=some_callback) |
In that case you could use a stub that delegates to the implementation.
def some_callback(): # I want to write this at the bottom
the_actual_callback() thread = threading.Thread(target=some_callback)
def the_actual_callback(): … |
A typical scenario is that several classes are declared in the same module because they mention each other and there’s no desire to resolve import cycles.
When the classes implement a large interface, the second class in the module may be visually lost at line > 500. A forward declaration would make it evident from the module heading what is contained there.
Most modern IDEs support a “structure view” which reveals what is in a module even if it’s > 2500 lines long.
Forward declarations add no new functionality, but just make it easier on the reader. Python already supports redefining any name in its core, but all linters will flag the redefinition.
Because the proposal requires no changes in syntax, it would be just a statement of purpose to be honored by linters.
Because def f(): ... is equivalent to def f(): pass, both valid Python, it’s up to core developers if they want to warn on err on a definition of ... with no concrete redefinition.
Linters warn if an implementation of a Protocol (which is different from an abstract class) is missing entries, but Python doesn’t care unless @runtime_checkable is used.
As I mentioned, I tried abusing typing.overload, but the linters complaint that my intentions are not up to norm.
I already mentioned that for mutual references the easy (probably the correct) thing to do is to declare the entities in the same module (when declared in different modules Python will have to suspend parsing one of the modules to get the dependencies right).
There are object model designs that are logical with mutual references. There are also well-known algorithms that have a logical implementation with recursion over two functions that call one another. Many times there are compelling reasons to place definitions in the same module.
Errors: If a forward declaration is made but no implementation is provided by the time the module is executed, the interpreter will raise a TypeError over the missing body.
I thought that meant that this code would raise a TypeError:
def f(): ...
f()
Though now that I read it more closely, you said “by the time the module is executed” as if this alone would raise an error:
def f(): ...
# end of file.
Can you clarify what situations would raise a TypeError?
Both of these options are not backward compatible because both are currently valid code that do not raise exceptions. The largest concern I have with @apalala’s proposal is it changes the semantics of valid code. I think a new syntax that is currently invalid is required to add this feature.
I’ve written functions, “right” or “wrong”, that use ellipses instead of pass; I prefer ‘…’ to ‘pass’. It might technically be wrong, but functionally it does exactly what is intended.
They express different intent. def func(): pass means “this function does nothing”; def func(): ... means “I haven’t written this function’s body yet”. They do the same thing NOW, but the ellipsis indicates that something isn’t finished yet.
Raising anything does change the semantics of module loading in Python.
I draw back the part of the proposal in which the Python compiler or the interpreter have to do anything differently or need any changes.
If this proposal makes it into a PEP, then it would be only about the expected behavior of type linters, which already jump on PEP8 over code that is completely valid Python.