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 anint. Mixed types →TypeErrorwith the offending index[String: Int]— every value must be anint. Wrong value →TypeErrorwith 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 aNonecase) - Works in collections:
[StringOrInt]accepts[1, "hello", 42]
Adding Custom Types
Conform your type to FromPyObject and/or IntoPyObject: