Skip to content

Type Conversion

ApplePy automatically converts between Swift and Python types using two protocols:

protocol FromPyObject {
    static func fromPython(_ obj: PyObjectPtr, py: PythonHandle) throws -> Self
}

protocol IntoPyObject {
    func intoPython(py: PythonHandle) -> PyObjectPtr
}

Type Mapping

Swift Type Python Type Direction
Int int
Double float
Float float ↔ (loses precision)
Bool bool
String str
Array<T> list ↔ (elements must conform)
Dictionary<K,V> dict ↔ (keys/values must conform)
Optional<T> T or None

CPython APIs Used

Conversion CPython Function
Swift Int → Python PyLong_FromLongLong
Python → Swift Int PyLong_AsLongLong
Swift Double → Python PyFloat_FromDouble
Python → Swift Double PyFloat_AsDouble
Swift Bool → Python PyBool_FromLong
Python → Swift Bool PyObject_IsTrue
Swift String → Python PyUnicode_FromString
Python → Swift String PyUnicode_AsUTF8
Swift Array → Python PyList_New + PyList_SetItem
Python → Swift Array PyList_Size + PyList_GetItem

Error Handling

If a type conversion fails, a PythonConversionError is thrown and surfaced as a Python TypeError:

enum PythonConversionError: Error {
    case typeMismatch(expected: String, got: String)
    case overflow(value: String, targetType: String)
    case collectionElement(collection: String, index: Int, key: String?, innerError: Error)
    case unionMismatch(got: String, expected: [String])
    case nullPointer
    case pythonError
}

Collection Safety

Collections are strict by default — every element must match the declared type. If any element fails, you get a precise error telling you exactly which element was wrong and what type it actually was.

  • [Int] — every element must be an int. Mixed types → TypeError with the offending index
  • [String: Int] — every value must be an int. Wrong value → TypeError with the offending key
  • If you actually want mixed types, use @PyUnion (see below) → e.g. [StringOrInt]
# Given: func process(values: [Int]) -> Int
>>> process([1, 2, 3])     # ✅ works
>>> process([1, "hello"])   # ❌ TypeError: list element 1: expected int, got str

This is the same approach PyO3 takes — homogeneous collections are strict, and you opt in to mixed types explicitly.

Union Types (@PyUnion)

For when you genuinely need to accept multiple types in one parameter. Declare a Swift enum, and ApplePy generates the type dispatch automatically.

import ApplePy

@PyUnion
enum StringOrInt {
    case string(String)
    case int(Int)
}

@PyFunction
func process(value: StringOrInt) -> String {
    switch value {
    case .string(let s): return "got string: \(s)"
    case .int(let n):    return "got int: \(n)"
    }
}
>>> process(42)        # "got int: 42"
>>> process("hello")   # "got string: hello"
>>> process(3.14)      # TypeError: 'float' cannot be converted to 'str | int'

How It Works

Under the hood, @PyUnion automatically adds FromPyObject and IntoPyObject conformances and generates the dispatch logic:

// Auto-generated by @PyUnion — tries variants in declaration order
extension StringOrInt: FromPyObject {}
extension StringOrInt: IntoPyObject {}

// Plus the method implementations inside the enum:
static func fromPython(_ obj: PyObjectPtr, py: PythonHandle) throws -> StringOrInt {
    if let v = try? String.fromPython(obj, py: py) { return .string(v) }
    if let v = try? Int.fromPython(obj, py: py) { return .int(v) }
    let typeName = String(cString: ApplePy_TYPE(obj).pointee.tp_name)
    throw PythonConversionError.unionMismatch(
        got: typeName, expected: ["str", "int"])
}

Handling None

Use Swift Optional to allow None in collections:

@PyFunction
func process(values: [StringOrInt?]) -> Int {
    // values can contain String, Int, or None
    // [1, "hello", None, 42] works
}

Design Notes

  • Variant order matters — first successful conversion wins
  • Each variant must have exactly one associated value conforming to FromPyObject, or no associated value (for a None case)
  • Works in collections: [StringOrInt] accepts [1, "hello", 42]

Adding Custom Types

Conform your type to FromPyObject and/or IntoPyObject:

extension MyType: FromPyObject {
    static func fromPython(_ obj: PyObjectPtr, py: PythonHandle) throws -> MyType {
        // Extract fields from Python object
    }
}

extension MyType: IntoPyObject {
    func intoPython(py: PythonHandle) -> PyObjectPtr {
        // Create and return a Python object
    }
}