Function Types¶
Function types represent the signature of callable values, including lambdas, method references, and delegate instances. They are used in type annotations for parameters, return types, fields, and type aliases.
Syntax¶
The function type syntax uses an arrow notation:
Examples¶
# No parameters, returns int
counter: () -> int
# Single parameter
processor: (str) -> int
# Multiple parameters
calculator: (int, int) -> int
# Returns None (void function)
callback: (str) -> None
# Nullable function type
handler: ((Event) -> None)?
# Function returning a function
factory: (str) -> ((int) -> bool)
# Generic function types (in type aliases)
type Callback[T] = (T) -> None
type Predicate[T] = (T) -> bool
type Transform[T, U] = (T) -> U
Parameter Names¶
Parameter names are optional in function type annotations. When provided, they serve as documentation only and are not part of the type signature:
# Without parameter names (preferred for brevity)
handler: (int, str) -> bool
# With parameter names (for documentation)
handler: (count: int, message: str) -> bool
# Both forms are equivalent types
# The names do not affect type compatibility
Note: Parameter names in function types do not create named parameter requirements at call sites. They are purely for readability and documentation.
type EventHandler = (sender: object, args: EventArgs) -> None
# All of these work - names are not enforced
def my_handler(s: object, a: EventArgs) -> None:
pass
def another_handler(obj: object, event_args: EventArgs) -> None:
pass
h: EventHandler = my_handler # OK
h = another_handler # OK
Function Types with None Return Type¶
Function types that indicate a function with no return value, i.e. -> None
must have the return type annotation -> None indicated. While it is true
that function definitions may omit this return type annotation if it is
-> None, function types of this sort on the other hand require it
for parsing/syntactic reasons.
No Optional Parameters in Function Type Annotations¶
Function type annotations cannot specify optional parameters (parameters with default values). All parameters in a function type annotation are required. Note that lambda expressions can have default parameters — this restriction applies only to the type annotation syntax:
# ❌ Invalid - cannot specify defaults in function type annotations
type BadCallback = (x: int, y: int = 0) -> int
# ✅ Valid - all parameters required
type GoodCallback = (int, int) -> int
# To accept functions with optional params, use the required-only signature
def process(callback: (int) -> int) -> int:
return callback(42)
# Functions with more parameters than required cannot be assigned
def add(x: int, y: int = 0) -> int:
return x + y
process(add) # ERROR: (int, int) -> int is not assignable to (int) -> int
# But you can wrap them in a lambda
process(lambda x: add(x)) # OK
Rationale: Function type annotations describe a calling convention -- what the caller must provide. Since the caller cannot know about default values, function type annotations represent the minimal required signature. This aligns with C# delegate semantics where all parameters are required.
Note: This restriction applies to the type annotation syntax
(int, int) -> int, not to lambda definitions themselves. Lambdas can have default parameter values in their definitions -- see Lambda Expressions.
Function Type Compatibility¶
A function type A is assignable to function type B if:
1. They have the same number of parameters
2. Parameter types are compatible in either direction (A's param assignable to B's, or B's param assignable to A's)
3. Return types are covariant (A's return type assignable to B's)
Design note: Parameter compatibility uses bidirectional assignability rather than strict contravariance. This is a deliberate choice that simplifies common callback patterns while remaining sound for the cases Sharpy supports (no mutable function-type containers that would expose the unsoundness). Strict contravariant checking is enforced at declaration sites via
VarianceValidatorfor interface and delegate type parameters -- see Generic Variance.
# Covariance in return types
type AnimalFactory = () -> Animal
type DogFactory = () -> Dog
dog_factory: DogFactory = lambda: Dog()
animal_factory: AnimalFactory = dog_factory # OK: Dog is subtype of Animal
# Bidirectional parameter compatibility
type AnimalHandler = (Animal) -> None
type DogHandler = (Dog) -> None
animal_handler: AnimalHandler = lambda a: print(a)
dog_handler: DogHandler = animal_handler # OK: Animal assignable to Dog's position (bidirectional)
Using Function Types¶
As parameter types:
def apply(value: int, transform: (int) -> int) -> int:
return transform(value)
result = apply(5, lambda x: x * 2) # 10
As return types:
def make_multiplier(factor: int) -> (int) -> int:
return lambda x: x * factor
doubler = make_multiplier(2)
print(doubler(5)) # 10
As field types:
class Button:
on_click: ((Button) -> None)?
def __init__(self):
self.on_click = None
def click(self) -> None:
if self.on_click is not None:
self.on_click(self)
In collections:
C# Mapping¶
Function types map to C# delegate types:
| Sharpy | C# |
|---|---|
() -> None |
Action |
(T) -> None |
Action<T> |
(T1, T2) -> None |
Action<T1, T2> |
() -> R |
Func<R> |
(T) -> R |
Func<T, R> |
(T1, T2) -> R |
Func<T1, T2, R> |
Implementation
- ✅ Native - Maps to System.Action<> and System.Func<> delegates.
Delegates vs Function Types¶
Function types ((T) -> R) and delegates (delegate F(x: T) -> R) both represent callable signatures, but serve different purposes:
- Function types are anonymous and map to
Func<>/Action<>. Use them for internal callbacks, higher-order function parameters, andtypealiases. - Delegates are named C# types. Use them when you need variance annotations (
in/out), event handler types, or a distinct named type for .NET interop.
# Function type via type alias — preferred for internal use
type Transform[T, U] = (T) -> U
# Delegate — use when variance or events require it
delegate Producer[out T]() -> T
When in doubt, start with a function type. Promote to a delegate only when you need a feature that function types cannot provide. See Delegates — When to use delegates and Type Aliases.