Skip to content

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 T in the C# Mapping column is the generated C# target syntax, not Sharpy source. In a .spy file, is compares references and never types: x is T draws SPY0349 and directs you to isinstance. 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.

# Valid — matches any list[T]
if isinstance(x, list):
    pass  # x could be list[int], list[str], etc.

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:

r: Iterator[str] = reversed("abc")
print("".join(r))   # cba

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) is float64 and prints 2.0. Python returns the unpromoted element (2). Returning the unpromoted element would require an object/union return, breaking type safety, so promotion is the type-safe choice (Axiom 1 .NET > Axiom 3 types > Axiom 2 Python). A non-callable key in 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:

enumerate(iterable, start=0)
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.