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¶
# Shorthand (preferred)
value: int? = Some(42)
empty: int? = None()
# Explicit (equivalent)
value: Optional[int] = Optional.Some(42)
empty: Optional[int] = Optional.None()
Note:
None()(with parentheses) constructs an empty Optional. BareNone(without parentheses) is the C# null literal forT | None(NullableType). 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().
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() |
None |
| Type safety | Works with any T |
Only reference types and Nullable<T> |
| Pattern matching | case Some(v): |
if x is not 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 ? 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
- Maybe Expressions - Converting
T | NonetoT? - Nullable Types -
T | Nonesyntax for .NET interop - Null Coalescing Operator - The
??operator - Pattern Matching - Pattern matching syntax