Variable Declaration and Assignment¶
Variables in Sharpy must be declared and assigned in a single statement. Variable declarations with type inference (Forms 2 and 3 below) are only allowed inside functions; module-level declarations require explicit type annotations.
There are three syntactic forms:
| Form | Syntax | Type Determination |
|---|---|---|
| Explicit type | x: int = 5 |
Type annotation specifies type |
| Inferred type | x = 5 |
Type inferred from initializer |
| Explicit inference | x: auto = 5 |
Type inferred from initializer (explicit) |
Form 1: Explicit Type Annotation
The type is explicitly specified. This form can be used both at module level (static fields) and inside functions:
# Module level (static fields) - explicit type REQUIRED
counter: int = 0
config: str = "default"
def main():
# Inside functions - explicit type allowed
count: int = 0
name: str = "Alice"
items: list[int] = [1, 2, 3]
user: User | None = None
Form 2: Type Inference (Implicit)
The type is inferred from the initializer expression. This form is only allowed inside functions, not at module level:
def main():
count = 0 # Inferred as int
name = "Alice" # Inferred as str
items = [1, 2, 3] # Inferred as list[int]
pi = 3.14159 # Inferred as float
# ❌ NOT allowed at module level:
# count = 0 # ERROR: module-level requires type annotation
Form 3: Type Inference (Explicit with auto)
The auto keyword explicitly requests type inference. This is functionally equivalent to Form 2 but makes the inference explicit. Like Form 2, this is only allowed inside functions:
def main():
count: auto = 0 # Inferred as int
name: auto = "Alice" # Inferred as str
items: auto = [1, 2, 3] # Inferred as list[int]
When to Use auto:
The auto keyword is primarily useful for variable shadowing, where you want to redeclare a variable with a different type:
def main():
x: int = 5
x = 10 # Assignment to existing int variable
x: str = "hello" # Shadowing: new variable of type str
x: auto = [1, 2, 3] # Shadowing: new variable, type inferred as list[int]
Bare Declarations (Declare-Then-Assign)¶
Sharpy allows variable declarations without initialization. The variable must be assigned on all control-flow paths before it is read — use-before-assign is a compile-time error (SPY0600):
def main() -> None:
x: int
if condition:
x = 1
else:
x = 2
print(x) # OK — x is assigned on all paths
y: int
print(y) # ERROR SPY0600 — y is used before being assigned
z: int
if condition:
z = 1
print(z) # ERROR SPY0600 — z is not assigned on the else path
Definite assignment follows the emitted control flow, and Sharpy's analysis is more precise
than C#'s in three places. A bare local it proves assigned is emitted with a definite
initializer (= default!) so the C# compiler agrees:
excepthandlers are entered from the state at thetrystatement's entry: a local assigned before thetryis definitely assigned in every handler,else,finally, and after the statement; a local assigned only inside the try body is not (the exception may be raised before the assignment). Struct fields assigned in__init__follow the same rule.- Loop
elsebodies run whenever the loop ends withoutbreak, so a local assigned in theelsebody (or on every path through the body and theelse) is definitely assigned after the loop. deferbodies run at scope exit, after every statement of the enclosing scope, so a deferred read of a local assigned later in that scope is definitely assigned (bodies run in LIFO order).
def risky(flag: bool) -> int:
if flag:
raise ValueError("boom")
return 1
def handlers(flag: bool) -> None:
x: int
y: int
x = 10
try:
y = risky(flag)
except ValueError:
print(x) # OK — x was assigned before the try
else:
print(x, y) # OK — y is assigned when the try body completed
def loop_else(items: list[int]) -> None:
x: int
for i in items:
x = i
else:
x = -1
print(x) # OK — the else body assigned x on the no-break path
def deferred() -> None:
x: int
defer:
print(x) # OK — runs at scope exit, after `x = 7`
x = 7
def handler_reads_try_body() -> None:
y: int
try:
y = risky(True)
except ValueError:
print(y) # ERROR SPY0600 — the exception may precede the assignment
Class and struct fields can also be declared without initialization if they are assigned in __init__.
Module-Level vs Function-Level Variables¶
See Program Entry Point for details on module-level declarations vs executable statements inside main().