Built-in Functions¶
Built-in functions provide polymorphic access to type behavior. They work uniformly on all types—primitives, .NET types, and Sharpy-defined types—by internally dispatching to the appropriate implementation:
- For Sharpy types: If the type defines the corresponding dunder method, the built-in function calls it
- For primitives and .NET types: The built-in function uses the native .NET operation
- Fallback behavior: Some functions provide sensible defaults when no custom implementation exists
This design allows code like len(x), str(x), and repr(x) to work consistently regardless of whether x is a list, a string, or a custom class.
Type Conversion¶
| Function | Purpose | C# Mapping |
|---|---|---|
int(x) |
Convert to integer (32-bit) | (int)x or Convert.ToInt32(x) |
float(x) |
Convert to float (64-bit) | (double)x |
str(x) |
Convert to string | Calls __str__ if defined, else .ToString() |
bool(x) |
Convert to boolean | Truthiness check |
Result-Returning Variants¶
For user input and other expected-failure scenarios, the type conversion functions offer Result-returning variants via static .parse() methods:
# Throwing version (Python-compatible)
n = int("42") # Raises ValueError if invalid
# Result-returning version (recommended for user input)
result: Result[int, ValueError] = int.parse("42")
match result:
case Ok(n):
print(f"Parsed: {n}")
case Err(e):
print(f"Invalid input: {e}")
# Similarly for float
f: Result[float, ValueError] = float.parse("3.14")
Guiding principle: Use the throwing version (int(x)) when bad input is a bug. Use the Result version (int.parse(x)) when bad input is expected (e.g., user input).
str(x) returns a C# string:
- For all types, calls .ToString()
- Primitive overloads (str(int), str(double), etc.) avoid boxing
Type Checking¶
| Function | Purpose | C# Mapping |
|---|---|---|
isinstance(x, T) |
Check if x is an instance of type T (second arg is a type position) |
x is T |
type(x) |
Get runtime type of x |
x.GetType() |
The
x is Tin the C# Mapping column is the generated C# target syntax, not Sharpy source. In a.spyfile,iscompares references and never types:x is Tdraws SPY0349 and directs you toisinstance. See Identity Operators.
type(x) Return Type:
The type() function returns System.Type, the .NET reflection type:
from system import Type
x = 42
t: Type = type(x) # Returns System.Int32 type
print(t.name) # "Int32"
print(t.full_name) # "System.Int32"
# Type comparison
if type(x) == type(0):
print("x is an integer")
# Prefer isinstance() for type checks
if isinstance(x, int): # More idiomatic
print("x is an integer")
Note: Unlike Python where type(None) returns NoneType, Sharpy's type(None) is a compile-time error because None is not a value with a type.
type() on Primitive Literals:
Unlike type(None), calling type() on primitive literals is valid and returns the corresponding System.Type:
# All of these are valid
t1 = type(42) # System.Int32
t2 = type(3.14) # System.Double
t3 = type("hello") # System.String
t4 = type(True) # System.Boolean
t5 = type([1, 2, 3]) # Sharpy.Core.List`1[System.Int32]
# Only type(None) is an error
t6 = type(None) # ERROR: type(None) is not valid
This is because primitive literals are values with concrete runtime types, whereas None represents the absence of a value.
isinstance(x, T)
Checks whether x is an instance of type T at runtime. Returns True if x is an instance of T or any subclass of T. The second argument is a type position: it must name a type, and a successful check narrows the variable to that type in the branch.
value: object = get_value()
if isinstance(value, str):
# value is narrowed to str in this block
print(value.upper())
if isinstance(value, MyClass):
# value is narrowed to MyClass
value.my_method()
# Works with interfaces too
if isinstance(value, IDrawable):
# value is narrowed to IDrawable
value.draw()
Tuple Types:
Because the second argument is a type position, (A, B) denotes tuple[A, B] — the structural
ValueTuple type — not an any-of check. The check tests whether the value is a tuple[A, B]
at runtime and narrows to that tuple type on success.
This is a same-spelling-different-meaning deviation from Python, where isinstance(x, (int, str))
checks whether x is an int or a str. In Sharpy, isinstance(x, (int, str)) checks whether
x is a tuple[int, str]. See docs/deviations.yaml, entry isinstance-tuple-is-a-tuple-type.
x: object = (1, "hello")
# Structural tuple type test — (int, str) means tuple[int, str]
if isinstance(x, (int, str)):
# x is narrowed to tuple[int, str]
print(x[0]) # int
print(x[1]) # str
# Equivalent explicit spelling
if isinstance(x, tuple[int, str]):
print(x[0])
# Single-element tuple: (int) means tuple[int]
if isinstance(x, (int)):
print(x[0])
# The qualified spelling works identically (import builtins first)
if builtins.isinstance(x, (int, str)):
print(x[1]) # str — narrows exactly like the bare spelling
Checking Multiple Types:
To check whether a value is one of several types, use explicit or:
if isinstance(x, int) or isinstance(x, str):
# x could be int or str here
# Note: no automatic type narrowing in this case
pass
Non-type expressions in the second argument — including int or str — are refused with SPY0344:
# SPY0344 — the second argument must be a type expression
if isinstance(x, int or str): # SPY0344: not a type; for an any-of check
pass # write isinstance(x, int) or isinstance(x, str);
# (A, B) denotes the tuple type tuple[A, B]
Generic Types:
.NET reifies generics — List<int> and List<str> are distinct runtime types — so a generic type
must be named with its type arguments to name something testable. This is the reverse of Python,
where generics are erased: CPython accepts the open form isinstance(x, Box) and rejects
isinstance(x, Box[int]), and Sharpy does the opposite.
# Valid — the closed spelling names a runtime type, and narrows to it
if isinstance(x, Box[int]):
pass
if isinstance(x, dict[str, int]):
pass
# Valid — the bare name is accepted when the value's own static type fills the vector
b: Box[int] = Box[int](5)
if isinstance(b, Box): # tests Box[int]
pass
# SPY0345 — nothing here determines Box's type arguments
y: object = make_box()
if isinstance(y, Box):
pass
An unparameterized builtin collection is the one exception: list, set and dict written without
type arguments test against their non-generic protocol interface (Sharpy.IList/ISet/IDict),
which every instantiation implements, so the check succeeds for any element type.
This matches the behavior of the C# is operator the check lowers to — x is Box<int> (C#) is
exact, and x is Box (C#) does not compile. Both spellings there are C# target syntax: in Sharpy
source the type test is always isinstance, since is compares references (SPY0349).
Type Narrowing:
When isinstance() is used in a conditional, the variable's type is narrowed within that branch:
def process(value: object) -> str:
if isinstance(value, str):
return value.upper() # OK: value is str
if isinstance(value, int):
return str(value * 2) # OK: value is int
return "unknown"
Implementation: Maps to C# is pattern matching with type narrowing.
Iterator Functions¶
| Function | Purpose | C# Mapping |
|---|---|---|
next(iterator) |
Get next value from iterator | Calls MoveNext() + Current; raises StopIteration if exhausted |
next(iterator, default) |
Get next value or default | Calls MoveNext() + Current; returns default if exhausted |
it = iter([1, 2, 3])
print(next(it)) # 1
print(next(it)) # 2
print(next(it)) # 3
print(next(it, -1)) # -1 (iterator exhausted, returns default)
next(it) # Raises StopIteration
Iterator-Returning Builtins¶
The following builtins return Iterator[T] — a lazy, single-pass value:
| Function | Returns | Description |
|---|---|---|
map(f, iterable) |
Iterator[U] |
Apply f to each element |
filter(f, iterable) |
Iterator[T] |
Keep elements where f returns True |
zip(a, b) |
Iterator[tuple[A, B]] |
Pair elements from two iterables |
enumerate(iterable) |
Iterator[tuple[int, T]] |
Index + value pairs |
reversed(iterable) |
Iterator[T] |
Elements in reverse order |
iter(iterable) |
Iterator[T] |
Explicit iterator from any iterable |
range is not in this list — it returns range, its own sequence type that supports
multiple iterations and length queries.
Laziness and single-pass semantics. Iterator values are consumed exactly once. A second traversal yields nothing:
it: Iterator[str] = map(str, [1, 2, 3])
print("".join(it)) # 123
print("".join(it)) # (empty — iterator exhausted)
The Iterator[T] annotation. Laziness is expressed in the type system via the
Iterator[T] spelling. This is the annotation to use when a binding holds the return value of
any iterator-returning builtin:
Repr contract. Iterator reprs are address-free: CPython embeds object addresses
(<map object at 0x...>), but Sharpy prints the type tag alone. range has a deterministic
CPython repr and matches it exactly. iter() prints the generic <iterator object> without
naming its source container (see deviation iterator-repr-no-address in
docs/deviations.yaml).
| Expression | Sharpy repr | CPython repr |
|---|---|---|
map(f, xs) |
<map object> |
<map object at 0x...> |
filter(f, xs) |
<filter object> |
<filter object at 0x...> |
zip(xs, ys) |
<zip object> |
<zip object at 0x...> |
enumerate(xs) |
<enumerate object> |
<enumerate object at 0x...> |
reversed(xs) |
<reversed object> |
<list_reverseiterator object at 0x...> |
iter(xs) |
<iterator object> |
<list_iterator object at 0x...> |
range(0, 3) |
range(0, 3) |
range(0, 3) |
Collection Functions¶
| Function | Purpose | C# Mapping |
|---|---|---|
len(x) |
Get length | Calls __len__ if defined, else .Count or .Length |
min(iter) |
Minimum value | .Min() or Math.Min() |
max(iter) |
Maximum value | .Max() or Math.Max() |
sum(iter) |
Sum values | Builtins.Sum() |
sorted(iter) |
Sort collection | Builtins.Sorted<T>() → List<T> |
reversed(iter) |
Reverse | Builtins.Reversed<T>() → Iterator<T> |
enumerate(iter) |
Index + value | .Select((x, i) => (i, x)) |
min() and max() Signatures:
The min() and max() functions accept an optional default parameter for empty iterables:
min() and max() accept an iterable (with an optional default for empty iterables and an
optional key function), or two-or-more scalar values directly (the variadic value form):
| Form | Description |
|---|---|
min(iterable) |
Minimum value; raises ValueError if empty |
min(iterable, default=value) |
Minimum value; returns default if empty |
min(iterable, key=f) |
Minimum by comparison key f |
min(a, b, ...) |
Minimum of two or more scalar values (value form) |
min(a, b, ..., key=f) |
Minimum of two or more values by comparison key f |
max(iterable) |
Maximum value; raises ValueError if empty |
max(iterable, default=value) |
Maximum value; returns default if empty |
max(iterable, key=f) |
Maximum by comparison key f |
max(a, b, ...) |
Maximum of two or more scalar values (value form) |
max(a, b, ..., key=f) |
Maximum of two or more values by comparison key f |
numbers = [3, 1, 4, 1, 5]
print(min(numbers)) # 1
print(max(numbers)) # 5
empty: list[int] = []
print(min(empty, default=0)) # 0 (empty iterable, returns default)
print(max(empty, default=-1)) # -1
min(empty) # Raises ValueError
# Variadic value form: two or more scalar values.
print(min(3, 1, 2)) # 1
print(max("a", "bbb", "cc", key=len)) # "bbb"
Mixed-numeric value form — Python divergence. When the value form mixes numeric types, Sharpy promotes to a common type (using the same rules as the binary numeric operators), so
min(2, 3.0)isfloat64and prints2.0. Python returns the unpromoted element (2). Returning the unpromoted element would require anobject/union return, breaking type safety, so promotion is the type-safe choice (Axiom 1.NET> Axiom 3types> Axiom 2Python). A non-callablekeyin the value form is a compile-time error (SPY0230).
"The same rules as the binary numeric operators" means literally the same table — C# §12.4.7, as
tabulated in Numeric Type Promotion. The call's
return type is the promoted type, and the emitted call carries it as an explicit type argument
(min(a, b) below emits Sharpy.Builtins.Min<long>(a, b)), so the .NET overload that runs is the
one the promotion picked:
a: uint32 = 5
b: int16 = 4
r: int64 = min(a, b) # 4 — uint32 with int16 promotes to int64
m: int64 = max(a, b) # 5
# w: uint32 = min(a, b) # ERROR (SPY0220): Cannot assign type 'int64'
# # to variable of type 'uint32'
A pair the table refuses is refused here too, and the diagnostic names the function and both argument types:
c: uint64 = 5
d: int32 = 4
# print(min(c, d)) # ERROR (SPY0220): Cannot determine common numeric type
# # for 'min' with argument types 'uint64', 'int32'
Cast one argument to the type you want the comparison to happen in (int64(c) or uint64(d)).
enumerate() Signature:
The enumerate() function takes the iterable and an optional positional start argument:
| Form | Description |
|---|---|
enumerate(items) |
Indices start at 0 |
enumerate(items, 1) |
Indices start at 1 |
enumerate(items, start=1) |
Indices start at 1 (keyword form also accepted) |
names = ["Alice", "Bob", "Charlie"]
# Default: start at 0
for i, name in enumerate(names):
print(f"{i}: {name}") # 0: Alice, 1: Bob, 2: Charlie
# Start at 1 (positional argument)
for i, name in enumerate(names, 1):
print(f"{i}. {name}") # 1. Alice, 2. Bob, 3. Charlie
Implementation: 🔄 Lowered - .Select((x, i) => (i + start, x)).
sum() Integer Widths:
sum accepts every integer width. The result type is the element's C# arithmetic width:
| Element type | Result type | Notes |
|---|---|---|
int8, uint8, int16, uint16 |
int32 |
C# promotes sub-int arithmetic to int |
int32 |
int32 |
|
uint32 |
uint32 |
|
int64 |
int64 |
|
uint64 |
uint64 |
|
float32, float64 |
same |
Each width has a start form: sum(xs, start) initialises the accumulator to start and adds
elements onto it. Overflow raises OverflowError (checked accumulation, matching the int
form's LINQ Sum overflow behaviour).
| zip(a, b) | Combine iterables | .Zip() |
| range(n) | Number sequence | Enumerable.Range() |
range() Signature:
The range() function matches Python's signature exactly:
| Form | Description | Example |
|---|---|---|
range(stop) |
0 to stop-1 | range(5) → 0, 1, 2, 3, 4 |
range(start, stop) |
start to stop-1 | range(2, 5) → 2, 3, 4 |
range(start, stop, step) |
start to stop-1, by step | range(0, 10, 2) → 0, 2, 4, 6, 8 |
# Single argument: 0 to n-1
for i in range(5):
print(i) # 0, 1, 2, 3, 4
# Two arguments: start to stop-1
for i in range(2, 7):
print(i) # 2, 3, 4, 5, 6
# Three arguments: start to stop-1, stepping by step
for i in range(0, 10, 2):
print(i) # 0, 2, 4, 6, 8
# Negative step for countdown
for i in range(10, 0, -1):
print(i) # 10, 9, 8, 7, 6, 5, 4, 3, 2, 1
Implementation: 🔄 Lowered - Simple forms use for (int i = start; i < stop; i += step), complex forms use Enumerable.Range() or generator.
| filter(pred, iter) | Filter | .Where() |
| map(func, iter) | Transform | .Select() |
| all(iter) | All truthy | .All() |
| any(iter) | Any truthy | .Any() |
len(x) returns the number of items in a container:
- For Sharpy types with __len__: calls __len__
- For collections: uses .Count property
- For strings/arrays: uses .Length property
I/O Functions¶
| Function | Purpose | C# Mapping |
|---|---|---|
print(x) |
Print to console | Console.WriteLine() |
input(prompt) |
Read from console | Console.ReadLine() |
Mathematical Functions¶
| Function | Purpose | C# Mapping |
|---|---|---|
abs(x) |
Absolute value | Math.Abs() |
pow(x, y) |
Power | Math.Pow() |
round(x, n) |
Round | Math.Round() |
divmod(a, b) |
Quotient + remainder | (a / b, a % b) |
divmod() Return Types:
The divmod() function returns a tuple containing the quotient and remainder. The return type depends on the operand types, following the same numeric promotion rules as / and //:
| Operand Types | Return Type | Notes |
|---|---|---|
Both int (32-bit) |
tuple[int, int] |
Integer division and modulo |
Any int64 |
tuple[int64, int64] |
Promoted to int64 |
Any float32/float64 |
tuple[float64, float64] |
Float division |
Any float |
tuple[float, float] |
Single-precision float division |
divmod(17, 5) # (3, 2) - tuple[int, int]
divmod(17L, 5) # (3L, 2L) - tuple[int64, int64]
divmod(17.0, 5.0) # (3.0, 2.0) - tuple[float, float]
Object Functions¶
| Function | Purpose | C# Mapping |
|---|---|---|
repr(x) |
Debug representation | Calls __repr__ if defined, else __str__, else .ToString() |
hash(x) |
Hash code | Calls __hash__ if defined, else .GetHashCode() |
id(x) |
Object identity | RuntimeHelpers.GetHashCode() |
repr(x) returns a string representation suitable for debugging:
- For Sharpy types with __repr__: calls __repr__
- Fallback: tries __str__, then .ToString()
- Typically includes type name and distinguishing attributes
hash(x) returns the hash code for use in dictionaries and sets:
- For Sharpy types with __hash__: calls __hash__
- For all types: falls back to .GetHashCode()
- If __eq__ is defined, __hash__ must also be defined (and vice versa)
Hashing Tuples:
Tuples are automatically hashable if all their elements are hashable:
# Tuples of hashable types can be hashed
point = (10, 20)
h = hash(point) # OK: both int elements are hashable
# Use tuples to create composite hash keys
coord_to_name: dict[tuple[int, int], str] = {}
coord_to_name[(0, 0)] = "origin"
coord_to_name[(10, 20)] = "point A"
# Nested tuples work if all elements hashable
nested = ((1, 2), (3, 4))
h = hash(nested) # OK
# Tuples containing unhashable types cannot be hashed
bad = ([1, 2], [3, 4]) # Tuple containing lists
h = hash(bad) # ERROR: list is not hashable
Implementation: 🔄 Lowered - Generated as method calls or type-appropriate dispatch.