Optional Type¶
T?is syntactic sugar forOptional[T]. TheT?shorthand is the preferred way to express optional values in Sharpy-native code.
The Optional[T] type is a special tagged union provided by the Sharpy standard library for representing values that may or may not be present. This is similar to Rust's Option type.
Optional[T] is a struct — no heap allocation for returning optional values, just a bool + value (like Nullable<T> but with tagged union semantics).
Definition¶
The Optional type is part of the standard library and provides special syntax and operators for ergonomic optional value handling.
Creating Optional Values¶
# Bare None (preferred)
value: int? = Some(42)
empty: int? = None
# None() (explicit alternative)
empty: int? = None()
# Fully qualified (equivalent)
value: Optional[int] = Optional.Some(42)
empty: Optional[int] = Optional.None()
Bare None and None() are interchangeable for T?. Bare None is the more ergonomic form. For T | None (C# nullable interop), only bare None is valid — see Nullable Types.
Pattern Matching¶
Use pattern matching to handle both Some and None cases:
def find_user(id: int) -> User?:
user = database.find(id)
if user is not None:
return Some(user)
return None
result = find_user(123)
match result:
case Some(user):
print(f"Found user: {user.name}")
case None:
print("User not found")
Common Methods¶
The Optional type provides several useful methods:
union Optional[T]:
case Some(value: T)
case None()
@property
def is_some(self) -> bool:
"""Returns True if the optional contains a value"""
match self:
case Some():
return True
case None:
return False
@property
def is_none(self) -> bool:
"""Returns True if the optional is empty"""
return not self.is_some
def unwrap(self) -> T:
"""Returns the value or raises an exception"""
match self:
case Some(value):
return value
case None:
raise Exception("Called unwrap on empty Optional")
def unwrap_or(self, default: T) -> T:
"""Returns the value or the default"""
match self:
case Some(value):
return value
case None:
return default
def unwrap_or_else(self, f: () -> T) -> T:
"""Returns the value or calls f"""
match self:
case Some(value):
return value
case None:
return f()
def map(self, f: (T) -> U) -> U?:
"""Transforms the contained value if present"""
match self:
case Some(value):
return Some(f(value))
case None:
return None
Constructor Shorthand¶
When the expected type is known, you can use Some(value) and None
without qualifying with the type name:
# With type annotation - shorthand works
x: int? = Some(42)
y: int? = None
# Function return - shorthand works
def get_value() -> int?:
return Some(42)
# Default parameter - shorthand works
def foo(x: int? = None) -> None:
pass
# Without type context - error (type cannot be inferred)
x = Some(42) # Error: Cannot infer type for 'Some()'
The compiler infers the full type from context. The shorthand is equivalent to
calling Optional<T>.Some(value) or using Optional<T>.None().
Protocol Operations and Member Access¶
T? (Optional[T]) is strict: its safety guarantee is the whole point of the
type, so the underlying value is never reached implicitly. Protocol operations
(len(), the in membership test, indexing x[i], and iteration
for v in x) and direct member/method access on the underlying type are
compile errors on a T? receiver:
s: str? = get_name()
print(len(s)) # error: Optional type 'str?' does not support len() directly
print(s[0]) # error: Optional type 'str?' does not support indexing directly
print(s.upper()) # error: 'str?' has no member 'upper'
Reach the underlying value explicitly — narrow it, use ?., pattern-match, or
unwrap it:
# Narrow with `is not None` (refines T? to T in the branch)
if s is not None:
print(len(s))
print(s.upper())
# Null-conditional access (?.)
upper: str? = s?.upper()
# Pattern matching
match s:
case Some(v):
print(len(v))
case None:
print("empty")
# Unwrap (throws on None)
print(len(s.unwrap()))
print(s.unwrap_or("default"))
The only members callable directly on a T? are Optional's own API
(unwrap, unwrap_or, unwrap_or_else, map, is_some, is_none).
T | None is loose¶
By contrast, T | None (the C# nullable interop type — see
Nullable Types) is loose: protocol operations and
member access work directly on the underlying type, and a None/null
receiver fails at runtime (a NullReferenceException, mirroring Python's
TypeError / AttributeError on None). This matches .NET interop semantics,
where nullable references flow into ordinary member access:
s: str | None = dotnet_api()
print(len(s)) # works; throws at runtime if s is None
print(s.upper()) # works; throws at runtime if s is None
Choose T? when you want the compiler to force you to handle absence; choose
T | None at .NET interop boundaries where Python-parity runtime semantics are
acceptable.
Comparison: T? (Optional) vs T | None (C# Nullable)¶
| Feature | T? / Optional[T] |
T \| None (C# Nullable) |
|---|---|---|
| Meaning | Safe tagged union | C# nullable reference/value |
| Has value | Some(value) |
value |
| No value | None or None() |
None |
| Type safety | Works with any T |
Only reference types and Nullable<T> |
| Pattern matching | case Some(v): |
if x is not None: |
Protocol ops (len, in, [i], iteration) |
Compile error — narrow or unwrap() first |
Allowed — throws at runtime on None |
Member access (x.method()) |
Compile error — only the Optional API (unwrap, map, …) |
Allowed — throws at runtime on None |
| Heap allocation | No (struct) | No |
| Use case | Sharpy-native optionals | .NET interop boundaries |
| Interop | May need conversion | Direct .NET interop |
When to Use T? (Optional)¶
- You're writing Sharpy-native code
- You want explicit, type-safe optional semantics
- You're working with value types that need to be optional
- You prefer functional programming patterns (map, flatMap, etc.)
- You want to make optionality more explicit in the type system
When to Use T | None (C# Nullable)¶
- You're interfacing with .NET APIs that use null
- You're at a .NET interop boundary
- You want direct C# interop without conversions
See Nullable Types for details on T | None.
Examples¶
Safe Dictionary Access¶
def get_config_value(config: dict[str, str], key: str) -> str?:
if key in config:
return Some(config[key])
return None
# Using the result
value = get_config_value(config, "timeout")
match value:
case Some(v):
timeout = int(v)
case None:
timeout = 30 # default
Chaining Optional Operations¶
def get_user_city(user_id: int) -> str?:
user = find_user(user_id)
if user.is_none:
return None
address = user.unwrap().get_address()
if address.is_none:
return None
return Some(address.unwrap().city)
Transforming Optional Values¶
# Using map to transform the value if present
opt_number: int? = Some(42)
opt_string = opt_number.map(lambda x: f"The answer is {x}")
# Result: Some("The answer is 42")
opt_nothing: int? = None
opt_result = opt_nothing.map(lambda x: x * 2)
# Result: None
Converting Between Optional and C# Nullable¶
Use maybe to convert from T | None (C# nullable) to T? (Optional):
# C# nullable to Optional (use maybe)
raw: str | None = dotnet_api()
safe: str? = maybe raw # Convert to Optional[str]
# Optional to C# nullable
def optional_to_nullable(opt: T?) -> T | None:
match opt:
case Some(value):
return value
case None:
return None
See Maybe Expressions for details on the maybe keyword.
Implementation
- ✅ Implemented — Optional[T] is a struct-based tagged union in Sharpy.Core. Pattern matching with Some/None, the ? early-return operator, and maybe expressions are all supported.
Implementation Details¶
Optional[T] is implemented as a C# readonly struct in Sharpy.Core:
public readonly struct Optional<T>
{
// Two fields: the value and a hasValue flag
// Zero heap allocation
}
The static helpers Some(value) and None() are available at module scope
for convenient construction.
See Also¶
- Tagged Unions - General tagged union syntax and implementation
- Result Type - The Result type for error handling
- Question-Mark Operator - The
?early-return operator for propagatingNone - Maybe Expressions - Converting
T | NonetoT? - Nullable Types -
T | Nonesyntax for .NET interop - Null Coalescing Operator - The
??operator - Pattern Matching - Pattern matching syntax