PONYλM2Modula-2

Python.CodeCompared.To/Swift

An interactive executable cheatsheet comparing Python and Swift

Python 3.14 Swift 6.3
Output & Running
Hello, World
The two languages agree on the first line of the first program, which is rarer than it sounds and is a fair signal of how much else will feel familiar.
print("Hello, World!")
print("Hello, World!")
Swift files run top to bottom with no entry-point function, no class wrapper and no imports for the basics — a Swift script and a Python script have the same shape. Semicolons are optional and nobody writes them. The one thing to know early is that this is a compiled language: what looks like a script is compiled before it runs, and every example on this page had to satisfy a type checker before it printed anything.
Printing several values
Swift interpolates with a backslash and parentheses, and every string literal interpolates — there is no f prefix to remember or forget.
name = "Ada" age = 36 print(f"{name} is {age}") print(f"{name} will be {age + 1} next year") print(name, "is", age)
let name = "Ada" let age = 36 print("\(name) is \(age)") print("\(name) will be \(age + 1) next year") print(name, "is", age)
The interpolation can hold any expression, exactly as an f-string can, and it calls the value's description the way Python calls __str__. print also takes several arguments and joins them with a space, matching Python precisely, including the separator: and terminator: parameters that correspond to sep= and end=. Number formatting is the one place Swift is more awkward: f"{price:.2f}" becomes String(format: "%.2f", price).
Running it: a script that is really a compile
Swift keeps the shapes a Python developer expects — a runnable file, a REPL, a package manager — while doing something different underneath each of them.
# python script.py # python -c 'print(1 + 1)' # python <- the REPL print("Python: the file is the program")
// swift script.swift <- compiles, then runs // swift <- a REPL that works well // swift build && swift run <- a package print("Swift: compiled every time, even in script mode")
swift script.swift compiles the file and then runs it, so the feedback loop is slower than Python's and gets much slower on a real app, where a clean Xcode build of a mid-sized project takes minutes. The REPL is genuinely good and is worth using for exploration, which is unusual among compiled languages. The compile step is what buys the guarantees on the rest of this page, and whether that trade is worth it is the argument the whole language is having.
Value Semantics
A struct is copied; an object is shared
This is the single most valuable row on the page, and it is worth reading the two outputs before reading anything else. The code is the same shape in both columns and the results are not the same at all.
class Point: def __init__(self, x, y): self.x = x self.y = y first = Point(1, 2) second = first second.x = 99 print(first.x, second.x) # 99 99 — one object, two names
struct Point { var x: Int var y: Int } var first = Point(x: 1, y: 2) var second = first second.x = 99 print(first.x, second.x) // 1 99 — two independent values
In Python every name is a reference to an object, so second = first makes a second name for one thing and mutating through either is visible through both. A Swift struct is a value: assigning it, passing it to a function, or putting it in an array copies it, so the two variables cannot interfere. There is no aliasing to reason about, no defensive copy.deepcopy, and no question about whether a callee will mutate what you handed it. The copy is optimized away until something is actually written, so this costs nothing in the common case.
The same program written with a class
Swift has both kinds of type, and choosing between them is a real decision made on almost every type you declare. The difference is exactly the one from the previous row.
class Basket: def __init__(self): self.items = [] def add_apple(basket): basket.items.append("apple") basket = Basket() add_apple(basket) print(basket.items) # the callee changed the caller's object
struct BasketValue { var items: [String] = [] } final class BasketReference { var items: [String] = [] } func addApple(_ basket: BasketValue) -> BasketValue { var copy = basket copy.items.append("apple") return copy } func addApple(_ basket: BasketReference) { basket.items.append("apple") } let valueBasket = BasketValue() print(addApple(valueBasket).items, valueBasket.items) let referenceBasket = BasketReference() addApple(referenceBasket) print(referenceBasket.items)
A class in Swift behaves the way every Python object behaves: it is a reference, it is shared, and a function that receives one can change what the caller sees. A struct cannot, which is why addApple has to return a new value for the struct version. Swift's guidance is to reach for struct first and use class when you need identity, inheritance, or deliberate shared mutable state — the opposite default from Python, where there is only the one option. Notice also that a let struct is deeply immutable: you cannot call a mutating method on it or change any of its properties.
Why Swift has no mutable-default-argument bug
Every Python developer has met this bug once and has been writing into=None ever since. It is a direct consequence of references, and value semantics remove it from two directions at once.
def collect(item, into=[]): into.append(item) return into print(collect("a")) print(collect("b")) # ['a', 'b'] — the SAME list
func collect(_ item: String, into: [String] = []) -> [String] { var result = into result.append(item) return result } print(collect("a")) print(collect("b")) // ["b"] — a fresh value each time
Swift evaluates a default expression on every call rather than once at declaration, and the array is a value type that would be copied on assignment even if it did not. Note the second consequence, visible in the body: the parameter into is a let constant — Swift parameters are immutable — so mutating it requires an explicit local copy. That looks like extra work and is really the compiler making the copy visible where Python leaves it invisible and shared.
Arrays and dictionaries are values too
Swift's Array, Dictionary, Set and String are all structs, so the copy rule applies to them and not only to types you write yourself.
original = [1, 2, 3] other = original other.append(4) print(original) # [1, 2, 3, 4] settings = {"debug": True} copy_of_settings = settings copy_of_settings["debug"] = False print(settings) # {'debug': False}
var original = [1, 2, 3] var other = original other.append(4) print(original) // [1, 2, 3] var settings = ["debug": true] var copyOfSettings = settings copyOfSettings["debug"] = false print(settings) // ["debug": true]
This is where value semantics pay off most in day-to-day code. A function can take an array, and the caller knows it will come back unchanged; a property can be handed out without wrapping it in list(...); and there is no equivalent of the Python surprise where two dictionaries turn out to be one. The implementation is copy-on-write, so passing a million-element array is still a pointer copy until somebody writes to it. If you genuinely want shared mutable state, you reach for a class and it is visible in the declaration.
Computed properties and observers
What Python builds out of the @property decorator, Swift builds into the property declaration itself — and it adds two hooks Python has no form of.
class Rectangle: def __init__(self, width, height): self.width = width self._height = height @property def area(self): return self.width * self._height @property def height(self): return self._height @height.setter def height(self, value): print(f"height changed from {self._height} to {value}") self._height = value rectangle = Rectangle(3, 4) print(rectangle.area) rectangle.height = 10 print(rectangle.area)
struct Rectangle { var width: Int var height: Int { didSet { print("height changed from \(oldValue) to \(height)") } } var area: Int { width * height } } var rectangle = Rectangle(width: 3, height: 4) print(rectangle.area) rectangle.height = 10 print(rectangle.area)
A computed property is one with a body and no storage, which is @property; a set block gives it the setter half. The additions are willSet and didSet, which fire around a change to an ordinary stored property, so you can observe a plain field without converting it into a computed one and inventing a backing name. Python's equivalent requires the underscore-shadow dance in the left column, and every existing caller keeps working in both — which is the refactoring safety that made @property worth having in the first place.
Optionals
Optional is a type, not a value
In Python None can appear in any variable at any time and no annotation prevents it. In Swift, String and String? are different types and the compiler will not let you confuse them.
def find_name(user_id: int) -> str | None: return "Ada" if user_id == 1 else None name = find_name(2) try: print(name.upper()) # AttributeError at run time except AttributeError as error: print("AttributeError:", error)
func findName(_ userID: Int) -> String? { userID == 1 ? "Ada" : nil } let name = findName(2) // print(name.uppercased()) <- will not compile print(name?.uppercased() ?? "(nobody)")
String? is shorthand for Optional<String>, an enum with a .some(value) case and a .none case — so the absence is a wrapper around the value rather than a special value that can stand in for anything. That is the whole idea: because it is a different type, every place where absence is possible is visible in the signature, and every use of it has to say what happens when it is empty. It converts a run-time AttributeError into a compile error, which is the trade the rest of this section describes.
if let and guard let against a None check
Swift has two binding forms, and the difference between them is about where the happy path ends up on the page.
def find_name(user_id): return "Ada" if user_id == 1 else None def shout(user_id): name = find_name(user_id) if name is None: return "(nobody)" return name.upper() print(shout(1)) print(shout(2))
func findName(_ userID: Int) -> String? { userID == 1 ? "Ada" : nil } func shout(_ userID: Int) -> String { guard let name = findName(userID) else { return "(nobody)" } return name.uppercased() } if let found = findName(1) { print(found.uppercased()) } print(shout(2))
if let opens a block in which the unwrapped value exists; guard let binds it for the rest of the enclosing scope and requires its else branch to leave — return, throw, or break. That inversion is the point: a function written with guard handles every absent case at the top and then reads as straight-line code, which is the shape Python developers hand-roll with early returns. Swift also allows if let name with no = name, shadowing the name with its unwrapped self, which is what most modern Swift looks like.
Chaining, defaults, and the force unwrap
Three operators do the work Python spells out with conditional expressions, and one of them is a deliberate promise to the compiler that you can break.
class Address: def __init__(self, city): self.city = city class Customer: def __init__(self, address=None): self.address = address customer = Customer() print(customer.address.city if customer.address else "unknown") known = Customer(Address("London")) print(known.address.city)
struct Address { var city: String? } struct Customer { var address: Address? } let customer = Customer() print(customer.address?.city ?? "unknown") let known = Customer(address: Address(city: "London")) print(known.address!.city!)
?. short-circuits the whole chain to nil rather than crashing; ?? supplies a default and is the closest thing to value or default, except that it fires only on nil and not on empty strings or zero. ! is the force unwrap: it asserts the value is present and crashes the program if it is not, which is deliberately unpleasant to look at. Reviewers treat a ! as something to justify; the common exception is a value the compiler cannot know is present, such as an @IBOutlet wired up in a storyboard.
Types & Inference
let and var, and inference that is total
Swift declares every name with let or var, and the compiler works out the type from the initial value — so most Swift code carries about as few type annotations as Python does.
total = 0 for number in [1, 2, 3]: total += number print(total) LIMIT = 100 # a convention only LIMIT = 200 print(LIMIT) price: float = 20.0 print(price)
var total = 0 for number in [1, 2, 3] { total += number } print(total) let limit = 100 // limit = 200 <- will not compile print(limit) let price: Double = 20 print(price)
let is a constant binding and is the default a Swift programmer reaches for; the compiler warns when a var is never mutated. Inference is thorough enough that annotations appear mainly on function signatures, on stored properties, and where you want a type other than the obvious one — let price: Double = 20 above, since 20 alone would infer Int. There is no implicit numeric conversion at all: adding an Int to a Double does not compile, which is stricter than Python and catches a real class of unit mistakes.
Integers are fixed width and trap on overflow
Python integers grow to whatever the arithmetic needs. Swift's are machine words, and it makes an unusual choice about what happens at the edge.
big = 2 ** 62 print(big * 4) # arbitrary precision print(7 // 2) print(7 / 2) print(0.1 + 0.2)
let big = 1 << 62 print(big.multipliedReportingOverflow(by: 4).overflow) print(7 / 2) print(Double(7) / 2) print(0.1 + 0.2)
An Int is 64 bits on every platform Swift supports, and overflow does not wrap silently the way it does in C, Java, Kotlin and Go — it traps and crashes the program. That is a deliberate decision that a wrong answer is worse than a stopped process, and the &+ family and the ...ReportingOverflow methods exist for when you want to handle it yourself. Division follows the C convention, so 7 / 2 is 3 and matches Python's // rather than its /; and because there are no implicit conversions, mixing an Int and a Double requires writing Double(...) explicitly.
Tuples have names, and typealias is a real alias
Returning several values works the same way in both languages, and Swift lets you name the elements — which turns a positional tuple into something self-documenting.
def divide(numerator, denominator): return numerator // denominator, numerator % denominator quotient, remainder = divide(17, 5) print(quotient, remainder) Coordinate = tuple[float, float] here: Coordinate = (51.5, -0.1) print(here[0])
func divide(_ numerator: Int, by denominator: Int) -> (quotient: Int, remainder: Int) { (numerator / denominator, numerator % denominator) } let result = divide(17, by: 5) print(result.quotient, result.remainder) typealias Coordinate = (latitude: Double, longitude: Double) let here: Coordinate = (51.5, -0.1) print(here.latitude)
A named tuple member is addressed as result.quotient rather than result.0, so a two-value return needs neither a struct nor a comment explaining the order. Destructuring works too: let (quotient, remainder) = divide(17, by: 5). typealias is a compile-time name for an existing type, exactly like Python's type alias, and creates no new type — so it documents intent without preventing you from passing the wrong thing. When you want a distinct type, you declare a struct.
Strings
Counting characters: Swift counts what you see
This is a short row that prints wildly different numbers and teaches something true about both languages.
flag = "\U0001F1EC\U0001F1E7" family = "\U0001F468\u200D\U0001F469\u200D\U0001F467" print(len(flag)) print(len(family)) print(len("café"))
let flag = "\u{1F1EC}\u{1F1E7}" let family = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}" print(flag.count) print(family.count) print("café".count)
A Swift Character is an extended grapheme cluster — everything a reader would call one character, however many code points went into it — so a regional-indicator flag counts as 1 and a four-person family emoji counts as 1. Python counts Unicode code points, so the same strings come back as 2 and 5. Swift's answer is the one a user would give, and the price is that a String cannot be indexed by an integer: there is no text[3], because the third character is not at a known offset. You use String.Index, or Array(text) when you genuinely want random access.
String operations, renamed
Everything you do to a Python string has a Swift counterpart with a longer, more explicit name. The verbosity is a house style, not an accident.
sentence = " the quick brown fox " trimmed = sentence.strip() print(trimmed) print(trimmed.upper()) print(trimmed.split(" ")) print(trimmed.replace("quick", "slow")) print("fox" in sentence)
import Foundation let sentence = " the quick brown fox " let trimmed = sentence.trimmingCharacters(in: .whitespaces) print(trimmed) print(trimmed.uppercased()) print(trimmed.split(separator: " ")) print(trimmed.replacingOccurrences(of: "quick", with: "slow")) print(sentence.contains("fox"))
Swift's naming guidelines say that a method returning a new value reads as a noun phrase (uppercased(), trimmingCharacters, replacingOccurrences) while one that mutates in place reads as an imperative verb (uppercase(), sort()). Once you notice the rule, the name tells you whether the original changed — information Python leaves you to remember per method. split returns Substring values, which share storage with the original and are converted with String(...) when you need to keep them.
Slicing gives you a Substring, not a String
Swift has no integer slicing, and what its slicing operations hand back is a different type with a purpose.
path = "reports/2026/summary.csv" print(path[:7]) print(path[-4:]) print(path.split("/")[1])
let path = "reports/2026/summary.csv" print(path.prefix(7)) print(path.suffix(4)) let middle = path.split(separator: "/")[1] print(String(middle))
A Substring shares storage with the string it came from, so taking a prefix copies nothing — but it also keeps the whole original alive for as long as it exists, which is a memory leak waiting to happen if you store one. The type system makes that visible: a function taking a String will not accept a Substring, so you write String(middle) and the copy happens where you can see it. Python's slices always copy, which is simpler and occasionally much slower on large strings.
Collections
Arrays, dictionaries and sets, with element types
The literals look identical and the types underneath are not. A Swift array holds one type of element, and a dictionary holds one type of key and one type of value.
numbers = [1, 2, 3] numbers.append(4) print(numbers) person = {"name": "Ada", "age": 36} print(person["name"]) print(person.get("city", "unknown")) print(sorted({1, 2, 2, 3}))
var numbers = [1, 2, 3] numbers.append(4) print(numbers) let person: [String: String] = ["name": "Ada", "age": "36"] print(person["name"] ?? "?") print(person["city"] ?? "unknown") print(Set([1, 2, 2, 3]).sorted())
That homogeneity is why person above stores the age as a string: ["name": "Ada", "age": 36] has no single value type, and a mixed dictionary is what a struct is for. The other thing to notice is the ?? on every subscript: reading a missing key gives back an optional rather than raising a KeyError, so Swift's subscript behaves like dict.get() and there is no bracket form that crashes. Arrays are the reverse — an out-of-range index traps — because a missing index is a programming error while a missing key usually is not.
The comprehension becomes a chain
Swift has no comprehension syntax. The same work is a chain of methods, which reads in the order the work happens rather than putting the final transformation first.
people = [ {"name": "Ada", "age": 36}, {"name": "Bo", "age": 17}, {"name": "Cy", "age": 44}, ] names = [person["name"].upper() for person in people if person["age"] >= 18] print(names) print(sum(person["age"] for person in people))
struct Person { let name: String; let age: Int } let people = [Person(name: "Ada", age: 36), Person(name: "Bo", age: 17), Person(name: "Cy", age: 44)] let names = people.filter { $0.age >= 18 }.map { $0.name.uppercased() } print(names) print(people.reduce(0) { $0 + $1.age })
$0 is the implicit name for a closure's first argument ($1 for the second), which is why so much Swift reads as { $0.name }. The library is rich enough that most comprehensions have a shorter equivalent than a filter plus map: compactMap maps and drops nils in one step, first(where:) replaces a loop with a break, and Dictionary(grouping:by:) is defaultdict(list). For laziness — the generator-expression behavior — insert .lazy before the chain.
Iterating, with and without the index
The loop shapes match closely — enumerate becomes enumerated() and dictionary iteration yields pairs in both.
words = ["fig", "apple", "pear"] for index, word in enumerate(words): print(index, word) for word in reversed(words): print(word) ages = {"Ada": 36, "Bo": 17} for name, age in ages.items(): print(name, age)
let words = ["fig", "apple", "pear"] for (index, word) in words.enumerated() { print(index, word) } for word in words.reversed() { print(word) } let ages = ["Ada": 36, "Bo": 17] for (name, age) in ages.sorted(by: { $0.key < $1.key }) { print(name, age) }
The difference worth knowing is ordering. Python dictionaries have preserved insertion order since 3.7 and it is guaranteed by the language; a Swift Dictionary is a hash table with no defined order at all, and its order can differ between runs of the same program. Any Swift code whose output depends on dictionary order is a bug waiting to be found, which is why the example sorts. When order matters, you keep an array of keys or use an ordered-dictionary type from the Swift Collections package.
Dictionary operations without KeyError
Two of Python's most-used dictionary recipes have direct library support here, and neither of them needs a special dictionary subclass.
from collections import defaultdict words = ["fig", "apple", "pear", "plum"] by_length = defaultdict(list) for word in words: by_length[len(word)].append(word) print(sorted(by_length.items())) counts = {} for word in words: counts[word[0]] = counts.get(word[0], 0) + 1 print(sorted(counts.items()))
let words = ["fig", "apple", "pear", "plum"] let byLength = Dictionary(grouping: words, by: { $0.count }) print(byLength.sorted { $0.key < $1.key }) var counts: [Character: Int] = [:] for word in words { counts[word.first!, default: 0] += 1 } print(counts.sorted { $0.key < $1.key })
Dictionary(grouping:by:) is defaultdict(list) plus the loop, in one call with the types worked out. The subscript with a default: is the counting idiom: counts[key, default: 0] += 1 reads and writes through one lookup and needs no defaultdict, so an ordinary dictionary does the job and there is no risk of accidentally inserting a key just by reading it — the classic defaultdict surprise. merging(_:uniquingKeysWith:) is the third one, replacing {**a, **b} with an explicit rule for collisions.
Control Flow & Pattern Matching
switch is exhaustive and pattern-matching
Python's match, added in 3.10, was influenced by exactly this construct — so a reader who has used structural pattern matching will find Swift's switch immediately legible.
def describe(value): match value: case 0: return "zero" case int(n) if n < 0: return "negative" case str(text): return f"text of {len(text)}" case _: return "something else" print(describe(0)) print(describe(-5)) print(describe("abc"))
func describe(_ value: Any) -> String { switch value { case let number as Int where number == 0: return "zero" case let number as Int where number < 0: return "negative" case let text as String: return "text of \(text.count)" default: return "something else" } } print(describe(0)) print(describe(-5)) print(describe("abc"))
Both bind parts of the value, both take a guard clause (where rather than if), and both match on type. The difference is that Swift's switch must be exhaustive: leaving out a case that the compiler cannot prove is impossible is a compile error, not a silently missing branch. Over an enum that means adding a case breaks every switch that has not been updated, which is the strongest safety property in the language. There is also no fall-through — each case ends by itself — and ranges, tuples and optionals all have patterns of their own.
Ranges, and the two kinds of dots
Swift has two range operators, and having both is what lets the difference between them be visible instead of remembered.
for number in range(1, 6): print(number, end=" ") print() for number in range(10, 0, -2): print(number, end=" ") print() print(3 in range(1, 6))
for number in 1..<6 { print(number, terminator: " ") } print() for number in stride(from: 10, to: 0, by: -2) { print(number, terminator: " ") } print() print((1...5).contains(3))
1...5 includes both ends; 1..<6 excludes the upper one and is the exact equivalent of range(1, 6). Having the choice in the syntax removes the off-by-one guessing that a single convention forces. A stepped or descending sequence needs stride(from:to:by:) rather than a third argument, which is more to type and reads more clearly at the call site. Ranges are values, work on any Comparable type, and can be used as switch patterns — case 90...100: in a grading switch is idiomatic.
Matching a single case with if case
A full switch is heavy when you care about exactly one case. Swift lets a pattern appear in an if, a guard, a while or a for.
def render(result): match result: case ("ok", value): return f"ok: {value}" case _: return "not ok" print(render(("ok", "data"))) print(render(("error", "timeout")))
enum FetchResult { case success(String) case failure(reason: String) } let result = FetchResult.success("data") if case .success(let value) = result { print("ok: \(value)") } func describe(_ outcome: FetchResult) -> String { guard case .success(let value) = outcome else { return "not ok" } return "ok: \(value)" } print(describe(.failure(reason: "timeout")))
if case .success(let value) = result reads oddly at first — the pattern is on the left of the = — and it is the same machinery a switch case uses, so it binds associated values the same way. guard case is the early-exit form and is how a function insists on one particular case before continuing. Python's match requires the full statement even for one case, so the equivalent is a match with a single arm and a case _: pass, which is why most Python code uses isinstance instead.
Functions & Argument Labels
Argument labels are part of the name
This is the syntactic feature that makes Swift look unlike everything else, and it comes straight from Objective-C. Every parameter has two names: one the caller writes and one the body uses.
def move(target, from_position, to_position): return f"{target}: {from_position} -> {to_position}" print(move("piece", "e2", "e4")) print(move(target="piece", from_position="e2", to_position="e4"))
func move(_ target: String, from origin: String, to destination: String) -> String { "\(target): \(origin) -> \(destination)" } print(move("piece", from: "e2", to: "e4"))
The label is mandatory at the call site unless the parameter is declared with _, which is the opposite of Python, where every argument may be passed positionally. That is why the function above is referred to as move(_:from:to:) — the labels are part of its identity, and two functions differing only in labels are two different functions. The payoff is that a Swift call reads as a sentence without the caller having to remember to write the names; the cost is that renaming a label breaks callers, so the labels are designed as carefully as the function name.
Defaults, and the parameter that can be written
Default values work as they do in Python. What has no Python counterpart is inout, which exists precisely because value semantics would otherwise make this function impossible.
def scale(values, factor=2): for index in range(len(values)): values[index] *= factor numbers = [1, 2, 3] scale(numbers) print(numbers) # the callee mutated the caller's list
func scale(_ values: inout [Int], by factor: Int = 2) { for index in values.indices { values[index] *= factor } } var numbers = [1, 2, 3] scale(&numbers) print(numbers)
Since a Swift array is copied when passed, a function that wants to modify the caller's array must say so with inout, and the caller must agree by writing & at the call. The mutation is therefore visible in both places — you can tell from the call site alone that numbers may change, which is information a Python reader simply does not have. Under the hood it is copy-in, copy-out rather than a pointer, so there is no aliasing to reason about either.
Overloading: several functions with one name
Swift picks a function by its full signature, so several functions may share a name as long as their parameter or return types differ.
from functools import singledispatch @singledispatch def describe(value): return f"something: {value}" @describe.register def _(value: int): return f"an integer: {value}" @describe.register def _(value: str): return f"a string of {len(value)}" print(describe(7)) print(describe("abc")) print(describe(1.5))
func describe(_ value: Int) -> String { "an integer: \(value)" } func describe(_ value: String) -> String { "a string of \(value.count)" } func describe(_ value: Double) -> String { "something: \(value)" } print(describe(7)) print(describe("abc")) print(describe(1.5))
Python has one function per name, and getting type-based dispatch requires functools.singledispatch and a registration for each type — which works at run time, on the first argument only. Swift resolves the overload at compile time using every argument and even the expected return type, so let count: Int = convert(text) can select a different function from let ratio: Double = convert(text). The cost is that a large overload set slows the type checker noticeably and produces error messages that are famously unhelpful when nothing matches.
Closures
Closures can hold statements
A Swift closure is written in braces with the parameters before the keyword in, and — unlike a Python lambda — it can contain as many statements as it wants.
numbers = [3, 1, 2] print(sorted(numbers, key=lambda number: -number)) def apply_twice(function, value): return function(function(value)) print(apply_twice(lambda value: value + 1, 5)) print(apply_twice(lambda value: value * 2, 5))
let numbers = [3, 1, 2] print(numbers.sorted { $0 > $1 }) func applyTwice(_ function: (Int) -> Int, to value: Int) -> Int { function(function(value)) } print(applyTwice({ value in value + 1 }, to: 5)) print(applyTwice({ $0 * 2 }, to: 5))
That removes the recurring Python annoyance of a lambda outgrowing its single expression and having to become a named def. Everything else is shorthand: parameter names can be dropped in favor of $0 and $1, types can be inferred from context, and a single-expression body needs no return. Function types are written (Int) -> Int, the direct equivalent of Callable[[int], int], and are checked at compile time.
Trailing closures, and why Swift libraries look like syntax
When a closure is the last argument, Swift lets it be written after the closing parenthesis. It is a small rule with a large effect on how the language reads.
def timed(label, work): result = work() print(f"{label} finished") return result value = timed("job", lambda: sum(range(1000))) print(value)
func timed<T>(_ label: String, work: () -> T) -> T { let result = work() print("\(label) finished") return result } let value = timed("job") { (0..<1000).reduce(0, +) } print(value)
It is why numbers.sorted { … }, DispatchQueue.main.async { … } and every SwiftUI view body look like built-in constructs when they are ordinary function calls — and since Swift 5.3 several trailing closures can be chained with their labels, which is what makes animate { } completion: { } read the way it does. Python has no equivalent: a multi-statement callback must be a named function defined above the call, which is why the same designs show up there as decorators or context managers instead.
Capture is by reference, and that is a decision
Both languages capture variables rather than values by default, and only one of them gives you a way to say otherwise.
def make_counter(): count = 0 def increment(): nonlocal count count += 1 return count return increment counter = make_counter() print(counter(), counter()) # The late-binding trap: functions = [lambda: index for index in range(3)] print([function() for function in functions])
func makeCounter() -> () -> Int { var count = 0 return { count += 1 return count } } let counter = makeCounter() print(counter(), counter()) var index = 0 let capturedByValue = { [index] in index } index = 99 print(capturedByValue(), index)
Swift needs no nonlocal — a closure that assigns to a captured var simply does, and the variable outlives the function it was declared in. The addition is the capture list [index], which copies the value at the moment the closure is created; that is the fix for Python's classic late-binding surprise, where every lambda in a loop sees the loop variable's final value and the workaround is a default argument. The capture list is also where [weak self] goes, which is the subject of the memory section.
Protocols & Generics
Protocols against duck typing
Python asks whether a method exists at the moment you call it. Swift asks whether the type declared that it has one, at compile time, before the program runs.
class Duck: def speak(self): return "quack" class Robot: def speak(self): return "beep" def chorus(things): return " ".join(thing.speak() for thing in things) print(chorus([Duck(), Robot()])) # A thing with no speak() fails only when it is reached.
protocol Speaker { func speak() -> String } struct Duck: Speaker { func speak() -> String { "quack" } } struct Robot: Speaker { func speak() -> String { "beep" } } func chorus(_ things: [any Speaker]) -> String { things.map { $0.speak() }.joined(separator: " ") } print(chorus([Duck(), Robot()]))
A protocol is the contract; conforming to it is a declaration a type makes, and the compiler checks that every requirement is met. That is typing.Protocol with teeth: not a hint for a checker to consult, but a condition for the code to exist. Conformance can also be added later and from outside — extension Int: Speaker { } — so you can make a type you do not own satisfy a protocol you wrote, which is the piece Python has no answer for. Note any Speaker, which Swift 5.6 made explicit: it means "some value whose type is only known to conform", and it is boxed and dynamically dispatched.
Generics: some against any
A Python function that works on anything comparable needs no annotation at all. Swift needs to say what "comparable" means, and gets a compiled, specialized function in return.
def largest(items): result = items[0] for item in items[1:]: if item > result: result = item return result print(largest([3, 9, 2])) print(largest(["fig", "apple"])) # Anything comparable works; anything else fails at run time.
func largest<T: Comparable>(_ items: [T]) -> T { var result = items[0] for item in items.dropFirst() where item > result { result = item } return result } print(largest([3, 9, 2])) print(largest(["fig", "apple"]))
<T: Comparable> is a constraint: the function accepts any single type that supports <, and both calls above compile to separate specialized versions with no boxing and no dynamic dispatch. The distinction a Python reader most needs is some against any: some Speaker means "one specific type, decided at compile time, which the caller does not get to see", while any Speaker means "any conforming type, decided at run time" and pays for a box and a dispatch. some is what a SwiftUI body returns and is the one to prefer.
Protocol extensions give behavior for free
A Swift protocol can carry implementations as well as requirements, so shared behavior arrives without a base class and without inheritance.
class Greeter: def name(self): raise NotImplementedError def greeting(self): # shared behavior via a base class return f"Hello, {self.name()}" class French(Greeter): def name(self): return "Amélie" print(French().greeting())
protocol Greeter { func name() -> String } extension Greeter { func greeting() -> String { "Hello, \(name())" } } struct French: Greeter { func name() -> String { "Amélie" } } print(French().greeting())
This is the mechanism behind the phrase "protocol-oriented programming", and it is the answer to the problem Python solves with mixins and a method resolution order. Any type conforming to Greeter gets greeting() automatically and may override it; a struct can take part, which a base class cannot offer since structs do not inherit. It is also how the standard library works — most of what you can do to a Collection lives in an extension on the protocol rather than in each concrete type.
Codable: JSON without writing the mapping
Declaring conformance to Codable is usually the entire work of JSON encoding and decoding, because the compiler writes the mapping for you.
import json from dataclasses import dataclass, asdict @dataclass class Order: id: int items: list[str] order = Order(7, ["pen", "ink"]) text = json.dumps(asdict(order)) print(text) restored = Order(**json.loads(text)) print(restored.items[1])
import Foundation struct Order: Codable { let id: Int let items: [String] } let order = Order(id: 7, items: ["pen", "ink"]) let data = try JSONEncoder().encode(order) print(String(data: data, encoding: .utf8)!) let restored = try JSONDecoder().decode(Order.self, from: data) print(restored.items[1])
The synthesized code is generated at compile time from the stored properties, so encoding and decoding are type-checked rather than reflective — and decoding a payload with a missing or mistyped field throws rather than producing an object with a hole in it, which is what Order(**json.loads(text)) does not protect you from. Custom key names go in a nested CodingKeys enum, and keyDecodingStrategy = .convertFromSnakeCase handles the usual API convention in one line. Python's nearest equivalent is Pydantic, which is a library doing at run time what Swift does at compile time.
Extensions add methods to types you do not own
An extension adds methods, computed properties and protocol conformances to an existing type — including String, Int and types from Apple's frameworks.
# Built-in types cannot be extended at all. def is_palindrome(text): cleaned = text.lower().replace(" ", "") return cleaned == cleaned[::-1] def times(count, body): for index in range(count): body(index) print(is_palindrome("Never odd or even")) times(3, lambda index: print(f"tick {index}"))
import Foundation extension String { var isPalindrome: Bool { let cleaned = lowercased().replacingOccurrences(of: " ", with: "") return cleaned == String(cleaned.reversed()) } } extension Int { func times(_ body: (Int) -> Void) { for index in 0..<self { body(index) } } } print("Never odd or even".isPalindrome) 3.times { print("tick \($0)") }
This is monkey-patching's good half. Nothing is modified: the compiler resolves the call statically, so two libraries can each add an isPalindrome without one clobbering the other, and an extension cannot add stored properties or override existing methods. Python cannot extend a built-in at all — str.isPalindrome = … raises — so the equivalent is always a free function. Extensions are also the standard way to organize a large Swift type: the main declaration holds the stored properties, and each protocol conformance goes in its own extension.
Enums with Associated Values
Enums carry data, and switching on them is checked
A Swift enum case can carry values, which makes an enum a closed set of alternatives rather than a list of names. Python's Enum cannot do this; the nearest equivalent is a union of dataclasses.
from dataclasses import dataclass @dataclass class Success: value: str @dataclass class Failure: reason: str def render(result): match result: case Success(value): return f"ok: {value}" case Failure(reason): return f"failed: {reason}" return "unreachable?" print(render(Success("data"))) print(render(Failure("timeout")))
enum FetchResult { case success(String) case failure(reason: String) } func render(_ result: FetchResult) -> String { switch result { case .success(let value): return "ok: \(value)" case .failure(let reason): return "failed: \(reason)" } } print(render(.success("data"))) print(render(.failure(reason: "timeout")))
The switch above has no default and compiles, because the compiler can see the two cases are all of them. Add a third and every such switch in the program becomes a compile error pointing at exactly the code that has not been updated — the property Python cannot offer, since a match over a union is never checked for completeness and a missing case silently returns None. Note the leading-dot shorthand: once the type is known from context, .success("data") needs no FetchResult prefix.
Raw values, and behavior on the enum
Both languages let an enum carry a backing value and define behavior. The two conformances in the Swift declaration are doing the work Python's Enum base class does by default.
from enum import Enum class Priority(Enum): LOW = 1 HIGH = 3 def label(self): return "urgent" if self is Priority.HIGH else "whenever" for member in Priority: print(member.name, member.value, member.label()) print(Priority(3).name)
enum Priority: Int, CaseIterable { case low = 1 case high = 3 var label: String { switch self { case .high: return "urgent" case .low: return "whenever" } } } for member in Priority.allCases { print(member, member.rawValue, member.label) } print(Priority(rawValue: 3)!)
: Int gives each case a rawValue, and the initializer Priority(rawValue: 3) is failable — it returns an optional, because there may be no such case, where Python's Priority(3) raises. CaseIterable is what supplies allCases; iteration over an enum is opt-in rather than automatic. Swift enums may also have computed properties, methods and even initializers, but no stored properties, since the case itself is the storage.
Reference Counting & Cycles
Both languages count references — and one collects cycles
This is an unusual convergence for this site: CPython and Swift use the same underlying strategy, and they differ in what they do about its one weakness.
import gc class Node: def __init__(self, name): self.name = name self.other = None def __del__(self): print(f"{self.name} freed") first = Node("first") second = Node("second") first.other = second second.other = first # a cycle del first del second gc.collect() # the cycle collector cleans up
final class Node { let name: String var other: Node? init(name: String) { self.name = name } deinit { print("\(name) freed") } } func makeCycle() { let first = Node(name: "first") let second = Node(name: "second") first.other = second second.other = first // a cycle — neither is ever freed } makeCycle() print("scope ended; nothing was freed")
Both count references and destroy an object the instant its count reaches zero — so both give you deterministic cleanup, and deinit is __del__. The difference is the cycle. CPython runs a cycle collector in the background that finds unreachable loops and frees them, so the Python column above eventually prints its messages. Swift has no such collector at all: a reference cycle is a permanent leak, and it is your job to break it. That is why the next row exists, and why [weak self] is something every iOS developer types daily.
weak, unowned, and [weak self]
Swift gives you two keywords for a reference that does not keep its target alive, and using one of them is not an optimization — it is how the program avoids leaking.
import weakref class Parent: def __init__(self): self.child = None def __del__(self): print("parent freed") class Child: def __init__(self): self.parent = None def __del__(self): print("child freed") def build(): parent = Parent() child = Child() parent.child = child child.parent = weakref.ref(parent) # available, rarely needed build()
final class Parent { var child: Child? deinit { print("parent freed") } } final class Child { weak var parent: Parent? deinit { print("child freed") } } func build() { let parent = Parent() let child = Child() parent.child = child child.parent = parent // weak: no cycle } build()
weak is always optional and becomes nil automatically when the target is freed; unowned is not optional, is slightly faster, and crashes if you touch it after the target is gone, so it is for references you can prove outlive their use. The everyday appearance is the capture list [weak self] on a closure stored by the object it refers to — a view controller holding a completion handler that mentions self is the canonical cycle. Python has weakref and almost nobody reaches for it, because the cycle collector already handles the case.
Error Handling
throws is declared, and try is at the call site
Swift makes failure visible in two places Python leaves silent: in the function's signature, and at every call that might fail.
def read_port(text): return int(text) # nothing in the signature says this can fail try: print(read_port("8080")) print(read_port("eighty")) except ValueError as error: print("failed:", error)
enum ParseError: Error { case notANumber(String) } func readPort(_ text: String) throws -> Int { guard let value = Int(text) else { throw ParseError.notANumber(text) } return value } do { print(try readPort("8080")) print(try readPort("eighty")) } catch ParseError.notANumber(let text) { print("failed: not a number: \(text)") }
A function that can fail must be declared throws, and every call to one must be preceded by try — which is not a block, just a marker meaning "this line can throw". Reading Swift, you can see which lines can fail without knowing what the functions do. What Swift does not do is declare which errors, the way Java does, so it is not the checked-exception system its detractors sometimes assume. The other structural difference is that an error is any value conforming to the Error protocol, and an enum is the usual choice, which makes catching a specific case a pattern match.
try?, try!, and defer
Two suffixed forms of try collapse the common cases, and defer replaces finally with something that scales better.
def read_port(text): return int(text) value = None try: value = read_port("eighty") except ValueError: pass print(value if value is not None else -1) try: print("working") finally: print("cleanup runs last")
enum PortError: Error { case notANumber } func readPort(_ text: String) throws -> Int { guard let value = Int(text) else { throw PortError.notANumber } return value } let value = try? readPort("eighty") print(value ?? -1) func work() { defer { print("cleanup runs last") } print("working") } work()
try? turns a throwing call into an optional, discarding the error — the equivalent of except: pass, but confined to one expression. try! asserts it cannot fail and crashes if it does, and carries the same reviewer's eyebrow as a force unwrap. defer registers a block to run when the current scope exits by any route, and several of them run in reverse order, so cleanup sits next to the acquisition it undoes rather than in a finally far below. Python's nearest equivalent is a context manager, which is more powerful and much more ceremony for a one-line cleanup.
Result: an error as a value
When failure is expected rather than exceptional, Swift offers a type that carries it as a value — with the compiler insisting that both outcomes be handled.
def parse(text): try: return int(text), None except ValueError as error: return None, str(error) value, error = parse("eighty") print(value if error is None else "failed: notANumber") print(parse("21"))
enum ParseError: Error { case notANumber } func parse(_ text: String) -> Result<Int, ParseError> { guard let value = Int(text) else { return .failure(.notANumber) } return .success(value) } switch parse("eighty") { case .success(let value): print(value) case .failure(let error): print("failed: \(error)") } print(parse("21").map { $0 * 2 })
Result is an ordinary enum with a .success and a .failure case, so switching over it is exhaustive and there is no way to use the value without acknowledging the error. It supports map, flatMap and get() — the last of which throws, so you can move between the two styles freely. Python's tuple-of-value-and-error convention is the same idea with nothing enforcing it, since a caller can simply ignore the second element. The rule of thumb in Swift is throws for things that go wrong and Result for a failure you want to store, pass around, or hand to a callback.
Async, Await & Actors
async and await, spelled the same
The keywords are identical, the placement is identical, and the model underneath is not — which is the whole reason this section exists.
import asyncio async def fetch(name): await asyncio.sleep(0.01) return f"data for {name}" async def main(): print(await fetch("first")) asyncio.run(main())
func fetch(_ name: String) async -> String { try? await Task.sleep(nanoseconds: 10_000_000) return "data for \(name)" } let value = await fetch("first") print(value)
Both languages mark a suspending function async and require await at every call to one, so the "function coloring" problem is the same in both and the code looks nearly interchangeable. Swift needs no asyncio.run at the top of a script, since top-level code is itself an async context. The difference underneath is total: Python's coroutines all run on one thread with a single event loop, and Swift's run on a cooperative thread pool with as many threads as there are cores.
Running work concurrently — and actually in parallel
async let starts a child task immediately and binds its eventual result to a name; awaiting the names collects them. Structurally this is asyncio.gather.
import asyncio async def fetch(name, delay_seconds): await asyncio.sleep(delay_seconds) return f"{name} done" async def main(): results = await asyncio.gather( fetch("first", 0.03), fetch("second", 0.01), ) print(results) asyncio.run(main())
func fetch(_ name: String, delay: UInt64) async -> String { try? await Task.sleep(nanoseconds: delay) return "\(name) done" } async let first = fetch("first", delay: 30_000_000) async let second = fetch("second", delay: 10_000_000) let results = await [first, second] print(results)
The behavior differs where it matters most. Python's gather gives concurrency and never parallelism — one thread, one loop, and a CPU-bound coroutine blocks everything else — which is why heavy work goes to multiprocessing. Swift's child tasks are scheduled onto a real thread pool, so two of them genuinely run at the same instant on different cores; there is no GIL. For a dynamic number of tasks, withTaskGroup is the loop-shaped form, and it — like async let — will not let a child outlive the scope that created it.
Actors: the compiler checks your shared state
Because Swift's tasks run in parallel, mutable state shared between them is a real data race — and Swift answers with a language construct rather than a lock.
import asyncio class Counter: def __init__(self): self.value = 0 def increment(self): self.value += 1 # safe ONLY because of the GIL and one thread async def main(): counter = Counter() await asyncio.gather(*[asyncio.to_thread(counter.increment) for _ in range(100)]) print(counter.value) # may be less than 100 asyncio.run(main())
actor Counter { private var value = 0 func increment() { value += 1 } func current() -> Int { value } } let counter = Counter() await withTaskGroup(of: Void.self) { group in for _ in 0..<100 { group.addTask { await counter.increment() } } } print(await counter.current())
An actor is a reference type whose mutable state can only be reached from one task at a time; the compiler enforces it by making every access from outside async, which is why the calls above need await. That converts "remember to take the lock" into "the code does not compile otherwise". Python needs none of this while everything is on one event loop, and needs all of it the moment real threads are involved — where the GIL protects individual bytecodes but not a read-modify-write like value += 1. Swift 6's strict concurrency checking extends the same analysis to every value crossing a task boundary, via the Sendable protocol.
AsyncSequence against an async generator
A stream of values arriving over time is async for in Python and for await in Swift, and the loop reads almost identically.
import asyncio async def ticks(count): for number in range(count): await asyncio.sleep(0.01) yield number async def main(): async for value in ticks(3): print(value) asyncio.run(main())
func ticks(_ count: Int) -> AsyncStream<Int> { AsyncStream { continuation in Task { for number in 0..<count { try? await Task.sleep(nanoseconds: 10_000_000) continuation.yield(number) } continuation.finish() } } } for await value in ticks(3) { print(value) }
What differs is the producing side. Python's async generator is a function with yield in it and needs no scaffolding; Swift has no async generator syntax, so producing a stream means AsyncStream with a continuation you yield into and must remember to finish(). Consuming is where Swift catches up: AsyncSequence has map, filter, prefix and friends, and Apple's frameworks expose notifications, file lines and network responses as async sequences, so for await line in url.lines is ordinary code.
SwiftUI
A view is a value describing the screen
SwiftUI is the reason most Python developers ever open Swift, so it earns a short section — but it needs a UI frame to run, which the compiler on this page does not have. These rows are illustrative.
# The nearest Python shape: a template function returning markup, # re-rendered whenever the data changes. def counter_view(count): return f""" <div> <p>Count: {count}</p> <button>Increment</button> </div> """ print(counter_view(0).strip())
// import SwiftUI // // struct CounterView: View { // @State private var count = 0 // // var body: some View { // VStack { // Text("Count: \(count)") // Button("Increment") { count += 1 } // } // } // } print("A View is a struct; body describes the screen for the current state")
The mental model is the one a React or Jinja user already has: body is a pure function from the current state to a description of the screen, and the framework re-runs it when the state changes. It is a struct, so the entire view hierarchy is a value that gets rebuilt cheaply and diffed. The trailing-closure rule from the closures section is why VStack { … } and Button("Increment") { … } read as syntax; they are ordinary initializers taking closures.
State, and the property wrappers around it
The @ prefixes that cover a SwiftUI file are not framework-specific syntax — they are property wrappers, an ordinary Swift feature that any library can define.
# In a Python web framework, state lives on the server # or in the client's JavaScript, and re-rendering is # something you trigger explicitly. state = {"count": 0} state["count"] += 1 print("re-render with", state)
// @State private var count = 0 // owned by this view // @Binding var isOn: Bool // passed in, two-way // @Observable final class Model { ... } // a reference type the view watches // @Environment(\.colorScheme) var scheme // ambient, injected print("The @ prefixes are property wrappers, not framework magic")
A property wrapper is a type that intercepts reads and writes of a property, which is roughly what a Python descriptor does, and what @property and @cached_property are built from. Knowing that removes most of the mystery from SwiftUI: @State is a wrapper that stores the value outside the struct and tells the framework to re-run body when it changes. The names to learn are @State for state a view owns, @Binding for state it was lent, and the @Observable macro for a model object several views watch.
Packages & Tooling
Modules and import
A Swift import works at a coarser grain than Python's, and the practical consequence is that most files import almost nothing.
# billing/invoice.py # class Invoice: ... # # main.py # from billing.invoice import Invoice # # The import RUNS the file, once, and binds a module object. import math import time print(math.sqrt(16)) print(time.time() > 0)
// Every file in a target shares one namespace — no import between them. // import brings in a whole MODULE (a package target or a system library). import Foundation print(sqrt(16.0)) print(Date().timeIntervalSince1970 > 0)
There is no file-level namespace: every type declared anywhere in a build target is visible everywhere in that target with no import at all, so import appears only for other modules — Foundation, SwiftUI, a package dependency. You cannot import a single symbol in the usual case, and there is no module object to reference. Nothing executes at import time either, so there are no import-time side effects and no circular-import problem. Access control (private, fileprivate, internal, public) does the encapsulation work that a Python leading underscore only suggests.
Swift Package Manager against pip and a virtualenv
Swift's package manager ships with the toolchain and takes the approach a Python developer will recognize from Poetry or uv rather than from bare pip.
# python -m venv .venv && source .venv/bin/activate # pip install requests # pip freeze > requirements.txt # # The environment is a directory you ACTIVATE. print("Python: an interpreter, an activated environment, and pins")
// Package.swift // dependencies: [ // .package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0") // ] // // swift build <- resolves, writes Package.resolved, compiles // swift test print("Swift: a manifest in Swift, a lockfile, and no environment")
The manifest is itself a Swift program, so the dependency list is type-checked. Package.swift holds the version ranges you asked for and Package.resolved holds the exact commits — the split requirements.txt conflates. Dependencies are fetched into the project rather than into a shared environment, so there is nothing to activate and no way to run against the wrong one. There is no central registry: a dependency is a Git URL, which removes an entire class of name-squatting problem and adds the risk that a repository disappears.
Tests
Swift Testing, which replaced XCTest as the default in 2024, is much closer to pytest than the framework it succeeded.
# test_billing.py, run with: pytest def add(left, right): return left + right def test_add(): assert add(2, 3) == 5 test_add() print(add(2, 3))
// Tests/BillingTests/BillingTests.swift, run with: swift test // import Testing // // @Test func addsTwoNumbers() { // #expect(add(2, 3) == 5) // } func add(_ left: Int, _ right: Int) -> Int { left + right } print(add(2, 3))
#expect is a macro that takes an ordinary boolean expression and, when it fails, reports the values of the sub-expressions — the same trick that makes a bare assert useful in pytest, achieved with macros rather than bytecode rewriting. Tests are plain functions marked @Test rather than methods on a subclass, they can be async and throws, and parameterized tests take an argument list much as @pytest.mark.parametrize does. XCTest is still everywhere in existing projects, so you will read both.