Function and Method Overloading¶
Sharpy supports defining multiple functions or methods with the same name, provided their parameter signatures differ. This follows C# overloading semantics.
For the authoritative overload resolution rules (applicability and betterness, applicable to both functions and methods), see Overload Resolution.
Module-Level Function Overloading¶
Multiple def statements with the same name can appear at module level if they have different parameter signatures (arity or types). Unlike Python, which replaces previous definitions, Sharpy creates overloads — consistent with class method behavior.
def describe(x: int) -> str:
return "int:" + str(x)
def describe(x: str) -> str:
return "str:" + x
def describe(x: int, verbose: bool) -> str:
return f"int({x}, verbose={verbose})"
print(describe(42)) # "int:42"
print(describe("hello")) # "str:hello"
print(describe(42, True)) # "int(42, verbose=True)"
Module-level overloads are importable across files:
# main.spy
from lib import describe
print(describe(42)) # Resolves to describe(int) in lib
print(describe("hello")) # Resolves to describe(str) in lib
Implementation
- ✅ Native - C# supports method overloading directly. Each overload emits as a separate static method in the module's members class (<Stem>Module, see module_system.md).
Basic Method Overloading¶
Methods can be overloaded by parameter count (arity) or parameter types:
class Formatter:
def format(self, value: int) -> str:
return "int:" + str(value)
def format(self, value: str) -> str:
return "str:" + value
def format(self, value: int, width: int) -> str:
return str(value).rjust(width)
f = Formatter()
print(f.format(42)) # "int:42"
print(f.format("hello")) # "str:hello"
print(f.format(42, 10)) # " 42"
Implementation - ✅ Native - C# supports method overloading directly.
Constructor Overloading¶
Multiple __init__ methods can be defined with different signatures. Constructor chaining is supported via self.__init__():
class Point:
x: int
y: int
z: int
def __init__(self, x: int, y: int, z: int):
self.x = x
self.y = y
self.z = z
def __init__(self, x: int, y: int):
self.__init__(x, y, 0)
def __init__(self, x: int):
self.__init__(x, 0, 0)
Constructor chaining rules:
- self.__init__(...) in a constructor body chains to another constructor of the same class
- super().__init__(...) chains to the base class constructor
- Chaining calls are lowered to C# constructor initializers (: this(...) and : base(...))
Implementation
- 🔄 Lowered - self.__init__(...) → C# : this(...) constructor initializer
- 🔄 Lowered - super().__init__(...) → C# : base(...) constructor initializer
Overloading with Default Parameters¶
Be cautious when combining overloads with default parameters, as this can create ambiguity:
class Logger:
def log(self, msg: str) -> None:
print(msg)
def log(self, msg: str, level: int) -> None:
print(f"[{level}] {msg}")
logger = Logger()
logger.log("hello") # Calls log(str) — exact match
logger.log("hello", 2) # Calls log(str, int)
Avoid overloads that differ only in having additional defaulted parameters:
# ❌ Ambiguous — both match log("hello")
class BadLogger:
def log(self, msg: str) -> None: ...
def log(self, msg: str, level: int = 0) -> None: ...
Operator Method Overloading¶
Dunder methods for operators can be overloaded to accept different operand types:
class Vector:
x: float
y: float
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def __add__(self, other: Vector) -> Vector:
return Vector(self.x + other.x, self.y + other.y)
def __eq__(self, other: Vector) -> bool:
return self.x == other.x and self.y == other.y
Implementation - ✅ Native - Each dunder overload maps to a separate C# operator overload.
Modifiers Are Part of the Signature¶
An overload's identity is its whole parameter annotation: int, int?, int | None, int!str and list[int?] are five different signatures, at every host — a module def, a method, an __init__, an imported def. Resolution among them has one winner:
- A bare value is the identity match for
int. It never selects an Optional slot (1is not anint?), so next toint | Noneit selects the nullable overload. Some(v)andNone()select theT?overload; a bareNoneselectsT | None.Ok(v)/Err(e)select theT!Eoverload, and the constructor is typed by the slot it selected —Ok(1)passed toint!stris aResult[int, str], not a Result with an open error type.None()where no overload takes an Optional is refused by the constructor's own diagnostic (SPY0244), naming every slot the candidates offer.
def f(x: int) -> str:
return "int"
def f(x: int?) -> str:
return "opt"
def g(x: int?) -> str:
return "opt"
def g(x: int | None) -> str:
return "nullable"
def h(x: int) -> str:
return "int"
def h(x: int!str) -> str:
return "result"
print(f(1)) # int
print(f(Some(1))) # opt
print(f(None())) # opt
print(g(1)) # nullable — strict Optional: 1 is not an int?
print(g(None)) # nullable
print(g(Some(1))) # opt
print(g(None())) # opt
print(h(Ok(1))) # result
print(h(Err("e"))) # result
def k(x: int) -> str:
return "int"
def k(x: int | None) -> str:
return "nullable"
k(None()) # ❌ SPY0244: 'None()' can only construct Optional types, not 'int32' or 'int32 | None'
k(Some(1)) # ❌ SPY0220: Cannot pass argument of type 'int32?' to parameter of type 'int32' or 'int32 | None'
Two spellings that map to one C# parameter type are ONE signature and are refused at the declaration (SPY0701): float and double are both C# double; str and str | None are both C# string. int and int? map to int and Sharpy.Optional<int>, so they are distinct.
Implementation
- ✅ Native — each overload is its own C# method. A bare value bound to a T | None slot of an overloaded callee is emitted cast to that slot ((int?)1): C# admits T → Optional<T> implicitly and would otherwise find f(int?) and f(Optional<int>) ambiguous where Sharpy's strict Optional had already chosen.
Restrictions¶
- Cannot differ only by return type. Overloads must differ in parameter count or types. Two methods with identical parameter signatures but different return types are rejected.
# ❌ ERROR: Duplicate method signature
class Bad:
def process(self) -> int: ...
def process(self) -> str: ...
-
selfis excluded from signature comparison. The implicitselfparameter is not considered when comparing overload signatures. -
Overloads must be in the same class. A method in a subclass with the same name and signature as a base class method is an override, not an overload (use
@override).
Diagnostics¶
| Code | Level | Description |
|---|---|---|
| SPY0353 | Error | Ambiguous overload — multiple overloads match equally well |
| SPY0354 | Error | No matching overload — the candidates disagree (arity, or different failing arguments). A call every candidate rejects at the SAME argument reports SPY0220 instead; see Overload Resolution |
| SPY0355 | Error | Duplicate method signature — two overloads have identical parameter signatures |
| SPY0701 | Error | Duplicate CLR-mapped signature — two spellings that are one C# parameter type (float/double, str/str \| None) |
| SPY0244 | Error | None() passed to an overload set none of whose slots is an Optional |
See Also¶
- Function Parameters — General overload resolution rules and named argument interaction
- Constructors — Constructor definition and chaining
- Operator Overloading — Dunder methods for operator overloading