Skip to content

Type System

CSnakes provides seamless integration between Python and C# type systems through automatic and manual type conversion.

In Python, every object inherits from the base object type (PyObject). CSnakes has a PyObject class that represents all Python objects in C#. This class provides methods for attribute access, method invocation, and function calls, allowing you to interact with Python objects as if they were native C# objects.

The PyObject class in CSnakes has conversions to and from many C# types. You can also do anything you can do in Python (get attributes, call methods, call functions) on instances of PyObject

To make development easier, the CSnakes source generator generates the conversions (marshalling) calls to and from Python functions by using their type signatures.

Any of the supported types can be used in the PyObject.From<T>(T object) and T PyObject.As<T>(PyObject x) calls to marshal data from C# types into Python objects. PyObject instances contain a SafeHandle to the allocated memory for the Python object in the Python interpreter. When the object is disposed in C#, the reference is decremented and the object is released. C# developers don't need to worry about manually incrementing and decrementing references to Python objects, they can work within the design of the existing .NET Garbage Collector.

Supported Type Mappings

CSnakes supports the following typed scenarios:

Python type annotation Reflected C# Type
int long
float double
str string
bytes byte[]
bool bool
list[T] IReadOnlyList<T>
dict[K, V] IReadOnlyDictionary<K, V>
tuple[T1, T2, ...] (T1, T2, ...)
typing.Sequence[T] IReadOnlyList<T>
typing.Dict[K, V] IReadOnlyDictionary<K, V>
typing.Mapping[K, V] IReadOnlyDictionary<K, V>
typing.Tuple[T1, T2, ...] (T1, T2, ...)
typing.Optional[T] T?
T | None T?
typing.Generator[TYield, TSend, TReturn] IGeneratorIterator<TYield, TSend, TReturn>
typing.Buffer IPyBuffer 2
typing.Coroutine[TYield, TSend, TReturn] Task<TYield> 3
None (Return) void

Optional Types

CSnakes supports Python's optional type annotations from both Optional[T] and T | None (Python 3.10+):

def find_user(user_id: int) -> str | None:
    # Returns username or None if not found
    users = {1: "Alice", 2: "Bob"}
    return users.get(user_id)

def process_optional(value: int | None = None) -> str:
    if value is None:
        return "No value provided"
    return f"Value is {value}"

def optional_old_style(value: Optional[int] = None) -> None:
    pass

Generated C# signatures:

public string? FindUser(long userId);
public string ProcessOptional(long? value = null);
public void OptionalOldStyle(long? value = null);

Collections

Lists

def process_numbers(numbers: list[int]) -> list[str]:
    return [str(n * 2) for n in numbers]

def filter_positive(numbers: list[float]) -> list[float]:
    return [n for n in numbers if n > 0]
var numbers = new[] { 1, 2, 3, 4, 5 };
IReadOnlyList<string> result = module.ProcessNumbers(numbers);
// Result: ["2", "4", "6", "8", "10"]

var floats = new[] { -1.5, 2.3, -0.1, 4.7 };
IReadOnlyList<double> positive = module.FilterPositive(floats);
// Result: [2.3, 4.7]

Dictionaries

def word_count(text: str) -> dict[str, int]:
    words = text.split()
    return {word: words.count(word) for word in set(words)}

def user_lookup() -> dict[int, str]:
    return {1: "Alice", 2: "Bob", 3: "Charlie"}
var text = "hello world hello";
IReadOnlyDictionary<string, long> counts = module.WordCount(text);
// Result: {"hello": 2, "world": 1}

var users = module.UserLookup();
string userName = users[1]; // "Alice"

Tuples

CSnakes supports simple tuples as types up to 17 items:

def get_name_age() -> tuple[str, int]:
    return ("Alice", 30)

def get_coordinates() -> tuple[float, float, float]:
    return (12.34, 56.78, 90.12)
var (name, age) = module.GetNameAge();
Console.WriteLine($"{name} is {age} years old");

var (x, y, z) = module.GetCoordinates();
Console.WriteLine($"Position: ({x}, {y}, {z})");

Default Values

Python default values for types which support compile-time constants in C# (string, int, float, bool) are preserved in the generated C# methods:

def greet(name: str, greeting: str = "Hello", punctuation: str = "!") -> str:
    return f"{greeting}, {name}{punctuation}"

def calculate(value: float, multiplier: float = 2.0, add_value: int = 0) -> float:
    return value * multiplier + add_value

Generated C# methods:

public string Greet(string name, string greeting = "Hello", string punctuation = "!");
public double Calculate(double value, double multiplier = 2.0, long addValue = 0);

Usage:

// Use all defaults
string msg1 = module.Greet("Alice"); // "Hello, Alice!"

// Override some defaults
string msg2 = module.Greet("Bob", "Hi"); // "Hi, Bob!"

// Override all parameters
string msg3 = module.Greet("Charlie", "Hey", "?"); // "Hey, Charlie?"

Unsupported Types

See Roadmap for a list of unsupported types and possible alternatives.

Handling None

If you need to send None as a PyObject to any function call from C#, use the property PyObject.None:

env.MethodToCall(PyObject.None);

You can also check if a PyObject is None by calling IsNone() on any PyObject:

PyObject obj = env.MethodToCall();
if (obj.IsNone())
{
  Console.WriteLine("The object is None");
}

Python's type system is unconstrained, so even though a function can say it returns a int it can return None object. Sometimes it's also useful to check for None values.

Working with PyObject

For advanced scenarios, you can work directly with PyObject:

from typing import Any

def get_person() -> Any:
    return ... # an object of some sort
using CSnakes.Runtime.Python;

using PyObject obj = module.GetPerson();

// Check type
if (obj.HasAttr("keys"))
{
    // It's a dictionary-like object
    PyObject keys = obj.GetAttr("keys");
    // ... work with the object
}

See handling Python Objects for more details and examples.

Best Practices

1. Use Specific Types

# Good - specific types
def process_user_data(user_id: int, email: str) -> dict[str, str]:
    return {"id": str(user_id), "email": email}

# Avoid - too generic
def process_data(data: object) -> object:
    return data

2. Document Complex Return Types

def get_analysis_results() -> dict[str, list[tuple[str, float]]]:
    """
    Returns analysis results.

    Returns:
        Dictionary mapping category names to lists of (item_name, score) tuples.
    """
    return {
        "positive": [("item1", 0.8), ("item2", 0.9)],
        "negative": [("item3", 0.2)]
    }

3. Handle None Values

def safe_divide(a: float, b: float) -> float | None:
    return a / b if b != 0 else None
double? result = module.SafeDivide(10.0, 3.0);
if (result is null)
{
    Console.WriteLine("Division failed");
}
else
{
    Console.WriteLine($"Result: {result}");
}

Next Steps