Skip to content

Type Annotations

# Simple types
x: int = 42
name: str = "Alice"
flag: bool = True
pi: float = 3.14159

# Type inference (annotation optional when initializer present)
y = 42              # Inferred as int
pi = 3.14159        # Inferred as float

Implementation - ✅ Native - Direct mapping to C# type declarations.

Nullability and Optional Type Syntax

T           # Non-nullable type
T?          # Optional[T] — safe tagged union (Sharpy-native)
T | None    # C# nullable — .NET interop only
T !E        # Result[T, E] — for return type annotations
  • T? is syntactic sugar for Optional[T]. See Optional Type.
  • T | None is the only valid inline union form. No free unions like int | str. See Nullable Types.
  • T !E is syntactic sugar for Result[T, E], recommended for top-level return types. See Result Type.

None in Annotations

None is a value, not a type. It appears in an annotation only as a return type: -> None says a function returns no value, and that includes the return slot of a function type, (str) -> None. Any other annotation — a local, parameter, field, module variable, constant, type argument (list[None]), a function type's parameter, or None? — could only ever hold None, so it is refused with SPY0614. A slot that may hold None is spelled T | None (or T? for an Optional), and object holds any value.

def log(message: str) -> None:
    print(message)

def main() -> None:
    handler: (str) -> None = log
    count: int | None = None
    handler("count is " + str(count))

Output: count is None.

A modifier over None wraps nothing, so None?, None | None and None !E are refused in every position, a return type included: -> None? is SPY0614, and a function that returns no value says -> None. int | None is unaffected — its non-None side is a type.

A property is a value, so a property getter's -> None is a value position and is refused too, while a setter's -> None is a true return. The refusal is the annotation's only diagnostic: x: None = None reports SPY0614 alone, with no SPY0227 "cannot infer a type" for its initializer (an annotation that failed to resolve is error recovery, not a missing annotation).

def describe(x: None) -> str:   # SPY0614: 'None' is not a type; annotate ... as `T | None`, or use `object`
    return "nothing"
def nothing() -> None?:   # SPY0614: 'None' is not a type, so `None?` ... wraps nothing
    return None()
class Box:
    property get p(self) -> None:   # SPY0614: 'None' is not a type (a getter's return is the property's type)
        pass

Implementation - 🔄 Lowered - -> None is C# void. A value-position None has no C# spelling (void is not a value type), so it is refused at semantic analysis rather than reaching the C# compiler.

Shorthand Syntax

Sharpy supports shorthand syntax for common collection types. See Type Annotation Shorthand for details.

# Shorthand and canonical forms are equivalent:
items: [int] = [1, 2, 3]           # Shorthand for list[int]
scores: {str: int} = {}            # Shorthand for dict[str, int]
unique: {int} = {1, 2, 3}          # Shorthand for set[int]
point: (int, int) = (10, 20)       # Shorthand for tuple[int, int]