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
format(x, spec) Format with the format-spec mini-language Sharpy.PyFormat (Python's grammar)

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

format(x, spec) formats x with Python's format-spec mini-language — the grammar of an f-string hole's :spec and of str.format fields (see F-Strings). Width, precision and field indexes accept any Unicode decimal digit (category Nd), as in Python:

def main() -> None:
    print(format(3.14159, ".2f"))          # 3.14
    print(format(42, "08b"))               # 00101010
    print(format(1234567, ","))            # 1,234,567
    print("[" + format(7, "٥") + "]")      # [    7] — ARABIC-INDIC DIGIT FIVE is a width
    print("{0:>6}|{1:^5}".format("ab", 3)) #     ab|  3

An empty spec is str(x), except for a type that owns its spec (def __format__, or the CLR IFormattable): that type is asked for "", as Python calls type(x).__format__(x, ""):

class Money:
    cents: int

    def __init__(self, cents: int):
        self.cents = cents

    def __format__(self, spec: str) -> str:
        return "$" + str(self.cents // 100) + ("" if spec == "" else "|" + spec)

    def __str__(self) -> str:
        return "Money(" + str(self.cents) + ")"


def main() -> None:
    m = Money(1250)
    print(format(m, ""))      # $12
    print(format(m, "x"))     # $12|x
    print(str(m))             # Money(1250)
    print(format(3.5, ""))    # 3.5

A literal spec that cannot apply to its operand is refused at compile time with SPY0609, naming the operand's type as Sharpy spells it — decimal, not Python's decimal.Decimal (see docs/deviations.yaml, entry decimal-format-grammar); a spec known only at runtime raises ValueError with the same message:

def main() -> None:
    d: decimal = decimal(1.5)
    print(format(d, "d"))    # SPY0609: Unknown format code 'd' for object of type 'decimal'

A literal str.format template whose field can never be bound — a positional index past the arguments, or a keyword field (str.format binds positional arguments only) — is refused with SPY0613, where Python raises IndexError/KeyError at runtime:

def main() -> None:
    print("{0} {1}".format("a"))   # SPY0613: format field '{1}' cannot be bound: only 1 positional argument

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)      # SPY0386: type(None) is not supported; NoneType has no Sharpy equivalent

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.

interface IDrawable:
    def draw(self) -> None: ...

class MyClass(IDrawable):
    def my_method(self) -> None:
        print("my_method")

    def draw(self) -> None:
        print("draw")

def get_value() -> object:
    return MyClass()

def main() -> None:
    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()      # my_method

    # Works with interfaces too
    if isinstance(value, IDrawable):
        # value is narrowed to IDrawable
        value.draw()           # 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.

import builtins

def main() -> None:
    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])   # 1 — an int
        print(x[1])   # hello — a str

    # Equivalent explicit spelling
    if isinstance(x, tuple[int, str]):
        print(x[0])   # 1

    # Single-element tuple: (int) means tuple[int] — x is not one, so this prints nothing
    if isinstance(x, (int)):
        print(x[0])

    # The qualified spelling works identically
    if builtins.isinstance(x, (int, str)):
        print(x[1])   # hello — narrows exactly like the bare spelling

Checking Multiple Types:

To check whether a value is one of several types, use explicit or:

def describe(x: object) -> str:
    if isinstance(x, int) or isinstance(x, str):
        # x could be int or str here
        # Note: no automatic type narrowing in this case
        return "int or str"
    return "something else"

def main() -> None:
    print(describe(5))     # int or str
    print(describe(2.5))   # something else

Non-type expressions in the second argument — including int or str — are refused with SPY0344 (int or str currently also reports SPY0342 on each bare type name first, #2079):

# SPY0344 — the second argument must be a type expression
def check(x: object) -> None:
    if isinstance(x, 5):     # SPY0344: not a type; for an any-of check write
        pass                 # 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.

class Box[T]:
    value: T

    def __init__(self, value: T) -> None:
        self.value = value

def main() -> None:
    x: object = Box[int](5)

    # Valid — the closed spelling names a runtime type, and narrows to it
    if isinstance(x, Box[int]):
        print(x.value)                 # 5

    if isinstance(x, dict[str, int]):
        print("dict")                  # not printed

    # Valid — the bare name is accepted when the value's own static type fills the vector
    b: Box[int] = Box[int](7)
    if isinstance(b, Box):             # tests Box[int]
        print(b.value)                 # 7
class Box[T]:
    value: T

    def __init__(self, value: T) -> None:
        self.value = value

# SPY0345 — nothing here determines Box's type arguments
def check(y: object) -> None:
    if isinstance(y, Box):
        pass

Builtin collections are no exception — list, set and dict are generic and reified the same way. A bare isinstance(x, list) is accepted only when x's own static type fills the vector; on an open x it is refused SPY0345, exactly as Box is, and the message names the closed spelling to reach for (list[object] matches what json.loads and other object-typed producers build; a list[int] matches only a value constructed as list[int]):

# Valid — x's static type fills the vector
def f(xs: list[int]) -> None:
    if isinstance(xs, list):           # tests list[int]
        pass

# Valid — the closed spelling names the runtime type
def g(z: object) -> None:
    if isinstance(z, list[object]):
        pass
# SPY0345 — a bare open collection name determines no type arguments
def h(z: object) -> None:
    if isinstance(z, list):
        pass

The earlier type-erased protocol-interface behaviour (Sharpy.IList/ISet/IDict) is retired (#1708, #1619).

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
decimal decimal
bool int32 Python counts True as 1 — sum([True, True, False]) is 2

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).

start of another family. A start need not share the elements' family. The result is the row C# has for that pair of operand types, and nothing else — Sharpy adds no promotion rule of its own:

Elements start Result Example
any integer width, bool int the element's width (int32 for sub-int and bool) sum([1, 2], 1) → 4 (int32)
any integer width, bool, float32 float64 float64 sum([1, 2], 1.5) → 4.5
any integer width, bool decimal decimal sum([1, 2], decimal(1))
float32, float64 decimal refused (SPY0354) C# has no double + decimal row
any str refused (SPY0354) naming the pair

A start written as a constant converts first, as it does anywhere else: sum([1.5, 2.5], 1) is 5.0 because the literal 1 becomes 1.0 before the row is chosen. The keyword spelling sum(xs, start=v) dispatches identically to the positional one.

| 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.