Skip to content

Commit 3a30c70

Browse files
committed
PEP 8108: Introduce coroutinedispatch class for method overloading in functools
1 parent daa024d commit 3a30c70

File tree

1 file changed

+220
-0
lines changed

1 file changed

+220
-0
lines changed

peps/pep-8108.rst

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
PEP: 8108
2+
Title: Adding coroutinedispatch to functools for Method Overloading
3+
Author: Ricardo Robles <ricardo.r.f@hotmail.com>
4+
Status: Draft
5+
Type: Standards Track
6+
Created: 16-Nov-2025
7+
8+
Abstract
9+
========
10+
11+
This PEP proposes the addition of a `coroutinedispatch` class to the `functools` module in Python's standard library. The `coroutinedispatch` class enables method overloading based on argument types, supporting both synchronous and asynchronous methods. This feature simplifies the implementation of polymorphic behavior in Python, making it easier to write clean and maintainable code.
12+
13+
Motivation
14+
==========
15+
16+
Python currently lacks built-in support for method overloading based on argument types. While developers can achieve similar functionality using conditional statements or external libraries, these approaches often result in verbose and less readable code. Adding a `coroutinedispatch` class to the standard library would:
17+
18+
- Provide a standardized and Pythonic way to implement method overloading.
19+
- Enhance code readability and maintainability.
20+
- Support both synchronous and asynchronous methods, addressing a common limitation in existing third-party solutions.
21+
22+
Rationale
23+
=========
24+
25+
The `coroutinedispatch` class leverages Python's type hints and introspection capabilities to match method implementations based on argument types. This design aligns with Python's dynamic nature while encouraging the use of type annotations. By integrating this functionality into the `functools` module, we ensure consistency with other utility features for functional programming.
26+
27+
Specification
28+
=============
29+
30+
The `coroutinedispatch` class will be implemented as follows:
31+
32+
- **Registration of Methods**: Developers can register multiple implementations of a method using the `@coroutinedispatch` decorator. The decorator inspects the argument types of the decorated function and stores the implementation accordingly.
33+
- **Type Matching**: When the method is called, the `coroutinedispatch` class matches the provided arguments against the registered implementations. If no exact match is found, a `TypeError` is raised.
34+
- **Support for Async Methods**: The `coroutinedispatch` class distinguishes between synchronous and asynchronous contexts, allowing developers to define separate implementations for each.
35+
36+
Example Usage
37+
-------------
38+
39+
.. code-block:: python
40+
41+
import asyncio
42+
from functools import coroutinedispatch
43+
44+
class Example:
45+
@coroutinedispatch
46+
def process(self, x: int) -> str:
47+
return f"Processing integer: {x}"
48+
49+
@process.register
50+
async def _(self, x: int) -> str:
51+
await asyncio.sleep(0.1)
52+
return f"Processing integer asynchronously: {x}"
53+
54+
@process.register
55+
def _(self, x: str) -> str:
56+
return f"Processing string: {x}"
57+
58+
@process.register
59+
async def _(self, x: str) -> str:
60+
await asyncio.sleep(0.1)
61+
return f"Processing string asynchronously: {x}"
62+
63+
example = Example()
64+
print(example.process(42)) # Processing integer: 42
65+
print(example.process("hello")) # Processing string: hello
66+
67+
async def main():
68+
print(await example.process(42)) # Processing integer asynchronously: 42
69+
print(await example.process("hello")) # Processing string asynchronously: hello
70+
71+
asyncio.run(main())
72+
73+
Backwards Compatibility
74+
=======================
75+
76+
This proposal introduces a new class to the `functools` module and does not modify any existing functionality. As such, it is fully backwards compatible.
77+
78+
Security Implications
79+
=====================
80+
81+
The `coroutinedispatch` class relies on Python's type hints and introspection capabilities. It does not introduce any new security risks beyond those inherent to dynamic type checking.
82+
83+
How to Teach This
84+
=================
85+
86+
The `coroutinedispatch` class should be documented in the Python standard library reference, with examples demonstrating its usage. Tutorials and guides on functional programming in Python can include sections on `coroutinedispatch` to showcase its benefits.
87+
88+
Reference Implementation
89+
=========================
90+
91+
A reference implementation of the `coroutinedispatch` class is provided below:
92+
93+
.. code-block:: python
94+
95+
import inspect
96+
import asyncio
97+
from typing import Any, Callable, get_type_hints, get_origin
98+
from functools import lru_cache
99+
100+
101+
class coroutinedispatch:
102+
def __init__(self, func: Callable):
103+
self._sync_methods = {}
104+
self._async_methods = {}
105+
self._name = func.__name__
106+
107+
arg_types = self.get_arg_types(func)
108+
109+
if inspect.iscoroutinefunction(func):
110+
self._async_methods[arg_types] = func
111+
else:
112+
self._sync_methods[arg_types] = func
113+
114+
def get_arg_types(self, func: Callable) -> tuple:
115+
try:
116+
hints = get_type_hints(func)
117+
sig = inspect.signature(func)
118+
params = list(sig.parameters.values())
119+
120+
if params and params[0].name in ("self", "cls"):
121+
params = params[1:]
122+
123+
return tuple(hints.get(p.name, Any) for p in params)
124+
except Exception:
125+
return ()
126+
127+
@lru_cache(maxsize=128)
128+
def match_types(self, provided_args: tuple, expected_types: tuple) -> bool:
129+
if len(provided_args) != len(expected_types):
130+
return False
131+
132+
for arg, expected in zip(provided_args, expected_types):
133+
if expected is Any:
134+
continue
135+
136+
origin = get_origin(expected)
137+
if origin is not None:
138+
expected = origin
139+
140+
if not isinstance(arg, expected):
141+
return False
142+
143+
return True
144+
145+
@lru_cache(maxsize=128)
146+
def _find_matching_method(self, is_async: bool, *args: Any) -> Callable:
147+
"""Find the method that matches the argument types."""
148+
methods = self._async_methods if is_async else self._sync_methods
149+
150+
# Search for exact match
151+
for arg_types, method in methods.items():
152+
if self.match_types(args, arg_types):
153+
return method
154+
155+
# If no match, raise descriptive error
156+
arg_type_names = tuple(type(arg).__name__ for arg in args)
157+
context = "async" if is_async else "sync"
158+
available = list(methods.keys())
159+
160+
raise TypeError(
161+
f"No matching {context} method '{self._name}' found for "
162+
f"arguments: {arg_type_names}. Available: {available}"
163+
)
164+
165+
def register(self, func: Callable) -> Callable:
166+
"""Register a new method overload."""
167+
arg_types = self.get_arg_types(func)
168+
169+
if inspect.iscoroutinefunction(func):
170+
self._async_methods[arg_types] = func
171+
else:
172+
self._sync_methods[arg_types] = func
173+
174+
return self
175+
176+
def __call__(self, *args: Any, **kwargs: Any) -> Any:
177+
try:
178+
asyncio.get_event_loop()
179+
is_async_context = True
180+
except RuntimeError:
181+
is_async_context = False
182+
pass
183+
184+
# Exclude 'self' from arguments if present
185+
check_args = args[1:] if args and hasattr(args[0], self._name) else args
186+
187+
try:
188+
method = self._find_matching_method(is_async_context, *check_args)
189+
result = method(*args, **kwargs)
190+
191+
return result
192+
except TypeError:
193+
# If no async/sync method, try the other
194+
try:
195+
is_async_context = not is_async_context
196+
method = self._find_matching_method(is_async_context, *check_args)
197+
result = method(*args, **kwargs)
198+
199+
return result
200+
except TypeError:
201+
raise
202+
203+
def __get__(self, obj, objtype=None):
204+
"""Support for bound methods"""
205+
if obj is None:
206+
return self
207+
208+
import functools
209+
210+
return functools.partial(self.__call__, obj)
211+
212+
Acknowledgements
213+
================
214+
215+
Thanks to the Python community for their feedback and contributions to this proposal.
216+
217+
Copyright
218+
=========
219+
220+
This document has been placed in the public domain.

0 commit comments

Comments
 (0)