Basics & Syntax
Hello, World
One line each, and the Python one needs no import, no entry point and no type. The
if __name__ == "__main__" guard is shown here because it is the one piece of ceremony a Swift developer will meet immediately.print("Hello, World!")print("Hello, World!")Identical — and it is the last thing on this page that is. Both languages allow statements at file scope, so neither needs a
main. Python has no semicolons, no braces, and no type annotations required anywhere; indentation is not a style choice but the actual block structure, and getting it wrong is a syntax error.No let, no var, no types
🚨 There is no
let. Nothing in Python makes a binding unchangeable, and the naming convention is all the constness you get.let fixed = 1
var counter = 0
counter += 1
// fixed = 2 ← will not compile
let explicit: Int64 = 42
let name = "Ada"
print("\(name): \(fixed + counter), \(explicit)")
// A variable's type is fixed forever.
// name = 42 ← will not compile: cannot assign Int to String# No keyword at all. A name springs into existence on assignment.
counter = 0
counter += 1
# SCREAMING_CASE is a CONVENTION for a constant. Nothing enforces it: you can
# reassign it, and nobody will stop you. There is no 'let'.
FIXED = 1
name = "Ada"
print(f"{name}: {FIXED + counter}, {42}") # an f-string is the interpolation
# And a name can hold anything, at any time.
name = 42
print(name, type(name))The immutability discipline you have built in Swift has nowhere to live: there is no
let, and a SCREAMING_CASE constant is a convention that nothing enforces. A name is a label that can be reattached to any object of any type at any moment, so name = 42 after name = "Ada" is perfectly legal. F-strings (f"{name}") are the interpolation, and they are excellent — they take any expression, and f"{value=}" even prints the expression alongside its value, which is the fastest debugging tool in the language.Interpolation Becomes an f-String
Swift interpolates in every string literal. Python needs the
f prefix, and in exchange the braces take a format specification that Swift has to reach into Foundation for.import Foundation
let name = "widget"
let count = 7
print("\(count) x \(name)")
print(String(format: "%.2f", 12.5))
print(String(format: "%5d|", 42))
print("""
multi
line
""")name = "widget"
count = 7
print(f"{count} x {name}")
print(f"{12.5:.2f}")
print(f"{42:5d}|")
print("""multi
line""")The format specification after the colon is the piece worth learning, because it replaces every
String(format:) call: {value:.2f}, {count:,} for thousands separators, {name:>10} to right-align, {ratio:.1%} for a percentage. Two more things an f-string does that Swift cannot: f"{value=}" prints value=7, name and all, which is the fastest debugging print in the language; and {value!r} asks for the repr rather than the str. Python's triple-quoted string does not strip leading indentation the way Swift's does, which is why the example is written flush left.Indentation Is the Block Structure
Braces are gone and the indentation is not a convention — it is the syntax. A colon opens a block and the indent level closes it.
func describe(_ values: [Int]) -> String {
var parts: [String] = []
for value in values {
if value % 2 == 0 {
parts.append("\(value) even")
} else {
parts.append("\(value) odd")
}
}
return parts.joined(separator: ", ")
}
print(describe([1, 2, 3]))
// Braces mean the formatting is advisory: this compiles too.
func compact(_ n: Int) -> Int { if n > 0 { return n } else { return -n } }
print(compact(-4))def describe(values):
parts = []
for value in values:
if value % 2 == 0:
parts.append(f"{value} even")
else:
parts.append(f"{value} odd")
return ", ".join(parts)
print(describe([1, 2, 3]))
# There is no compact form: the indentation IS the structure, so a
# misplaced line is a different program, not a style complaint.
def compact(n):
return n if n > 0 else -n
print(compact(-4))The consequences a Swift developer notices in the first hour: there is no way to write a one-line function body with a real block in it, a stray space is an
IndentationError rather than nothing, and mixing tabs with spaces is a hard error since Python 3. Less obviously, this is why Python has no multi-line lambda — an anonymous function would need a block, and a block needs a line of its own — which is the real reason a trailing closure becomes a named def here. Four spaces is the universal convention and black is the tool that stops the argument.Dynamic Typing & Type Hints
Type hints look like Swift and check nothing
This is the concept that most misleads a Swift developer. Python's annotations use nearly the same syntax you already know — and the interpreter ignores them completely.
// The compiler enforces every one of these, before the program runs.
func double(_ number: Int) -> Int {
number * 2
}
print(double(21))
// print(double("nope")) ← compile error, caught at build time
struct User {
let name: String
let age: Int
}
let user = User(name: "Ada", age: 36)
print(user.name)
// print(user.nmae) ← compile error: no such property# The annotations are real syntax and completely optional. At RUN TIME they are
# stored as metadata and otherwise ignored — nothing checks them.
def double(number: int) -> int:
return number * 2
print(double(21))
print(double("nope")) # "nopenope" — the annotation lied, and Python did not care
# What DOES check them is a separate tool you run yourself: mypy, pyright, or
# your editor. In a codebase without one, the hints are documentation.
class User:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
user = User("Ada", 36)
print(user.name)
# A misspelled attribute is not an error until that LINE RUNS.
try:
print(user.nmae)
except AttributeError as error:
print("AttributeError:", error)Read the second
print in the right column again: double("nope") returns "nopenope", because * on a string repeats it and the int annotation is not enforced by anything. Type hints are checked by a separate tool — mypy or pyright, run in CI or by your editor — and a project that does not run one gets no checking at all. The practical advice for a Swift developer: annotate everything, turn on a strict type checker from day one, and treat it as the compiler you no longer have. It is genuinely good (gradual typing has come a long way), but it is opt-in, and it is not the language.String? Becomes str | None
The annotation for "this may be missing" reads almost exactly like Swift's, and it is checked by a tool you run separately rather than by the interpreter.
func firstLong(_ words: [String], minimum: Int = 4) -> String? {
words.first { $0.count >= minimum }
}
// The compiler REFUSES to let the result be used as a String.
if let found = firstLong(["a", "hello"]) {
print(found.uppercased())
} else {
print("none")
}
print(firstLong(["a"]) ?? "none")def first_long(words: list[str], minimum: int = 4) -> str | None:
return next((word for word in words if len(word) >= minimum), None)
found = first_long(["a", "hello"])
if found is not None:
print(found.upper())
else:
print("none")
# 🚨 Nothing at run time stops this. It raises AttributeError only
# because None has no .upper — a type checker would have said so first.
print(first_long(["a"]) or "none")str | None is the modern spelling of Optional[str] and means the same thing. What runs the check is mypy, pyright or your editor — never the interpreter, which discards the annotations into __annotations__ and carries on. 🚨 Two idioms to be careful with: if found: is not if found is not None:, because an empty string and 0 are falsy, and the same trap makes value or default replace a legitimate 0. Swift's ?? only fires on nil; Python's or fires on anything falsy.Generics, and Structural Protocols
Generic syntax has converged: Python 3.12 gained the
def name[T](…) form, so the declaration reads the way Swift's does. Only the enforcement differs.protocol Describable {
func describe() -> String
}
struct Point: Describable {
let x: Int, y: Int
func describe() -> String { "(\(x), \(y))" }
}
// The constraint is checked when this is COMPILED.
func announce<T: Describable>(_ items: [T]) {
for item in items { print(item.describe()) }
}
announce([Point(x: 1, y: 2), Point(x: 3, y: 4)])from typing import Protocol
class Describable(Protocol):
def describe(self) -> str: ...
class Point:
def __init__(self, x: int, y: int):
self.x, self.y = x, y
def describe(self) -> str:
return f"({self.x}, {self.y})"
# Point never mentions Describable — a Protocol matches STRUCTURALLY,
# which is duck typing that a type checker can verify.
def announce[T: Describable](items: list[T]) -> None:
for item in items:
print(item.describe())
announce([Point(1, 2), Point(3, 4)])typing.Protocol is the interesting half, because it is what Swift has no direct equivalent of: conformance is structural, so a class matches by having the right methods and never names the protocol. That means a type from a library you do not control satisfies your protocol automatically — which is the problem Swift solves with a retroactive extension, done here by not asking in the first place. None of it exists at run time: announce will happily take anything with a describe, and anything without one fails at the call.None Instead of Optionals
None is just a value
None is a value that any name may hold, so the compile-time distinction between String and String? simply does not exist here.func findName(_ id: Int) -> String? {
id == 1 ? "Ada" : nil
}
// The compiler REFUSES to let you use it without unwrapping.
print(findName(1)?.uppercased() ?? "(none)")
if let name = findName(1) {
print(name.count)
}
func shout(_ id: Int) -> String {
guard let name = findName(id) else { return "(nobody)" }
return name.uppercased()
}
print(shout(99))def find_name(id: int) -> str | None: # the hint says it may be None...
return "Ada" if id == 1 else None # ...and nothing enforces it
# There is no ?. and no ??. You check, by hand, or you crash.
name = find_name(99)
print(name.upper() if name is not None else "(none)")
# The walrus operator is the closest thing to 'if let'.
if (found := find_name(1)) is not None:
print(len(found))
def shout(id: int) -> str:
name = find_name(id)
if name is None: # the guard-let equivalent: an early return
return "(nobody)"
return name.upper()
print(shout(99))
# Forget the check and you get the Python equivalent of a nil crash:
try:
print(find_name(99).upper())
except AttributeError as error:
print("AttributeError:", error) # 'NoneType' object has no attribute 'upper'Absence is a value (
None) and nothing makes you check for it, so the safety Swift's optionals give you must be recovered with a type checker (str | None is enforced by mypy, and this is the strongest argument for running one). The idioms: x is None rather than x == None, an early return instead of guard let, and the walrus operator (:=) when you want to test and bind in one line. And the error you will meet on your first day is AttributeError: 'NoneType' object has no attribute '…' — that is Python's nil-crash, arriving at run time, in production, exactly where Swift would have refused to compile.There Is No ?.
🚨 The syntax a Swift developer reaches for most often has no Python equivalent at all. Every
?. becomes a check you write, or an exception you catch.struct Address { let city: String? }
struct Person { let address: Address? }
let people = [
Person(address: Address(city: "Oslo")),
Person(address: nil),
]
for person in people {
// One line, and it stops at the first nil.
print(person.address?.city?.uppercased() ?? "unknown")
}class Address:
def __init__(self, city):
self.city = city
class Person:
def __init__(self, address):
self.address = address
people = [Person(Address("Oslo")), Person(None)]
for person in people:
# Written out, because there is nothing shorter that is also correct.
city = person.address.city if person.address else None
print(city.upper() if city else "unknown")
# The other two shapes people reach for:
for person in people:
try:
print(person.address.city.upper())
except AttributeError:
print("unknown")The three real options, none of them as good as
?.: a conditional expression per level, which is correct and gets long; try/except AttributeError, which is short and 🚨 also swallows a genuine typo three levels down, so it hides the bug it looks like it is handling; and getattr(obj, "city", None), which is the same trade with a narrower blast radius. For dictionaries the picture is better — data.get("address", {}).get("city") chains cleanly — which is why deeply nested data is usually kept as dictionaries rather than objects in Python.guard let Becomes an Early Return
There is no
guard and no binding form. An early return does the same job, and the narrowing afterwards is a type checker's inference rather than something you declared.func initials(_ fullName: String?) -> String {
// guard binds for the rest of the scope, and the compiler
// enforces that the else branch leaves.
guard let name = fullName, !name.isEmpty else {
return "??"
}
let parts = name.split(separator: " ")
return parts.map { String($0.first!) }.joined()
}
print(initials(nil))
print(initials("Ada Lovelace"))def initials(full_name: str | None) -> str:
# No guard: return early, and nothing enforces that you did.
if not full_name:
return "??"
return "".join(part[0] for part in full_name.split())
print(initials(None))
print(initials("Ada Lovelace"))if not full_name: covers both of Swift's conditions at once, because an empty string is falsy — convenient here and the exact trap of the previous row when 0 or "" is a legitimate value. What is missing compared to guard is the compiler requiring the branch to leave: a Python if that falls through simply continues, so the shape is a convention. The walrus operator gives back the binding half — if (match := pattern.search(text)) is None: return — and is the closest thing to guard let the language has.Everything Is a Reference
There are no value types
The Swift habit that will bite you first. Assignment never copies — everything is a reference to a mutable object.
// A struct is a VALUE: assignment copies.
var first = [1, 2, 3]
var second = first
second.append(4)
print(first) // [1, 2, 3] — untouched
print(second) // [1, 2, 3, 4]
struct Point { var x: Int }
var pointA = Point(x: 1)
var pointB = pointA
pointB.x = 99
print(pointA.x, pointB.x) // 1 99
// And a function cannot mutate what you pass it, unless you say inout.
func addOne(_ items: [Int]) -> [Int] { items + [1] }
print(addOne(first), first)# Assignment binds a NAME to the same object. No copy, ever.
first = [1, 2, 3]
second = first
second.append(4)
print(first) # [1, 2, 3, 4] — mutated through the other name!
print(second)
# To copy, say so.
import copy
third = first.copy() # shallow
deep = copy.deepcopy(first) # and deepcopy for nested structures
third.append(99)
print(first, third)
# The classic beginner trap, and it catches experts too: a mutable DEFAULT
# argument is created ONCE, at definition time, and shared by every call.
def broken(item, items=[]):
items.append(item)
return items
print(broken(1)) # [1]
print(broken(2)) # [1, 2] ← the same list, still there
# The fix, and you will write it a thousand times:
def correct(item, items=None):
if items is None:
items = []
items.append(item)
return items
print(correct(1), correct(2))Python has no value types at all, so every assignment aliases and every function receives a reference to your object — mutate it inside and the caller sees it. The defenses are
.copy() (shallow) and copy.deepcopy() (recursive), and you must remember to reach for them. Then read the broken function again, because it is the single most famous trap in the language: a mutable default argument is evaluated once when the function is defined, so that list persists between calls and accumulates. Swift's value semantics make both of these unwritable; the Python idiom is items=None plus a check, and it is not optional.Copying Has to Be Asked For
A Swift
struct copies on assignment and its nested structs copy with it. Python copies nothing, and the two functions that do are not interchangeable.struct Inner { var values: [Int] }
struct Outer { var inner: Inner; var name: String }
var first = Outer(inner: Inner(values: [1, 2]), name: "first")
var second = first // a deep copy, all the way down
second.inner.values.append(3)
second.name = "second"
print(first.inner.values.count, second.inner.values.count)
print(first.name, second.name)import copy
class Inner:
def __init__(self, values):
self.values = values
class Outer:
def __init__(self, inner, name):
self.inner, self.name = inner, name
first = Outer(Inner([1, 2]), "first")
shallow = copy.copy(first) # a new Outer, the SAME Inner
shallow.inner.values.append(3)
shallow.name = "shallow"
print(len(first.inner.values), first.name) # 3 first — the list was shared
deep = copy.deepcopy(first) # a new everything
deep.inner.values.append(4)
print(len(first.inner.values), len(deep.inner.values))The distinction is the whole row.
copy.copy makes a new outer object whose attributes still point at the originals, so changing a nested list is visible through both — which is why shallow.name is private to the copy and shallow.inner.values is not. copy.deepcopy walks the whole graph, handles cycles, and is expensive. The everyday shorthands are the same trade: list(values), values[:] and dict(mapping) are all shallow. Swift never presents the choice, because a value type has no nested reference to share unless you put a class inside it.=== Becomes is
Swift has both questions and puts the identity one behind
===, which works only on classes. Python's is works on everything, which is what makes it easy to reach for by mistake.class Box { var value: Int; init(_ value: Int) { self.value = value } }
let first = Box(1)
let second = Box(1)
let alias = first
print(first === second) // false — different objects
print(first === alias) // true
// Structs have no ===; there is nothing to compare but the value.
struct Point: Equatable { let x: Int }
print(Point(x: 1) == Point(x: 1))class Box:
def __init__(self, value):
self.value = value
def __eq__(self, other):
return isinstance(other, Box) and self.value == other.value
first = Box(1)
second = Box(1)
alias = first
print(first is second) # False — different objects
print(first is alias) # True
print(first == second) # True — because __eq__ says so
# 🚨 And this is why 'is' looks like it works: small ints are shared.
print(256 is 256 - 0, 257 is 257 - 0)The rule with no exceptions:
is for None, True, False and sentinels you made yourself; == for every other question. Which small integers and strings happen to be shared is a CPython implementation detail that changes between versions, so code that passes its tests below 257 fails on a larger value — Python 3.8 and later emit a SyntaxWarning for is against a literal, which catches most of it. Note also that defining __eq__ without __hash__ makes the class unhashable, where Swift's Equatable and Hashable are separate on purpose.Functions
Functions, defaults, and *args
Parameters, defaults and variadics all have close counterparts — and Swift's argument labels, which are part of the method name, become optional keyword arguments.
func greet(name: String, salutation: String = "Hello") -> String {
"\(salutation), \(name)!"
}
func total(_ numbers: Int...) -> Int {
numbers.reduce(0, +)
}
print(greet(name: "Ada"))
print(greet(name: "Ada", salutation: "Hi"))
print(total(1, 2, 3))
// Closures, with the trailing-closure syntax.
let numbers = [1, 2, 3]
print(numbers.map { $0 * 2 })
let add: (Int, Int) -> Int = { $0 + $1 }
print(add(2, 3))def greet(name: str, salutation: str = "Hello") -> str:
return f"{salutation}, {name}!"
# *args collects positional arguments; **kwargs collects named ones.
def total(*numbers: int) -> int:
return sum(numbers)
def configure(**options):
return options
print(greet("Ada"))
print(greet("Ada", salutation="Hi")) # every parameter can be named — no labels
print(total(1, 2, 3))
print(total(*[4, 5, 6])) # * spreads a list into the arguments
print(configure(host="example.com", port=8080))
# Lambdas exist but are limited to ONE expression — no statements, no blocks.
# The idiomatic replacement is a comprehension or a named function.
numbers = [1, 2, 3]
print([number * 2 for number in numbers]) # not numbers.map { }
print(list(map(lambda number: number * 2, numbers)))
add = lambda first, second: first + second
print(add(2, 3))Named arguments and defaults are here and are better than Swift's in one respect: any parameter can be passed by name, with no argument label to declare.
*args is the variadic (and *list at a call site spreads a list into it, which is Swift's missing spread operator), while **kwargs collects arbitrary named arguments — a thing Swift cannot express at all. The real adjustment is that lambda is one expression only: there are no multi-statement closures, so the trailing-closure style you write constantly in Swift becomes either a comprehension (the idiomatic choice) or a named function defined above.Argument Labels Become Optional Keywords
Swift's labels are part of the method's name and the caller must write them. Python's keyword arguments are optional at every call site — and the API author can make them mandatory in either direction.
// The labels ARE the name: this function is move(from:to:).
func move(from origin: Int, to destination: Int) -> String {
"moved \(origin) to \(destination)"
}
print(move(from: 1, to: 9))
// print(move(1, 9)) — will not compile
// _ opts a label out, per parameter.
func doubled(_ value: Int) -> Int { value * 2 }
print(doubled(4))def move(origin, destination):
return f"moved {origin} to {destination}"
print(move(1, 9))
print(move(origin=1, destination=9))
print(move(destination=9, origin=1)) # order stops mattering
# / and * are the markers that take the choice back.
def clamp(value, /, *, low=0, high=10):
return max(low, min(high, value))
print(clamp(15, high=12))
# clamp(value=15) -> TypeError: value is positional-only
# clamp(15, 0, 12) -> TypeError: low and high are keyword-onlyThe two markers are worth memorizing because they are how a Python API expresses what Swift expresses with labels. Everything before
/ is positional-only — the caller may not name it, which frees you to rename it later without breaking anyone. Everything after * is keyword-only — the caller must name it, which is how you stop clamp(15, 0, 12) from being three anonymous integers. A consequence Swift developers miss at first: because labels are not part of the name, Python cannot overload on them at all; one name is one function, and functools.singledispatch is the deliberate workaround.Trailing Closures, and What Replaces Them
🚨 A Python
lambda holds one expression and nothing more, so Swift's multi-statement trailing closure becomes a named function — and the decorator is the pattern that grew up around that limit.let values = [3, 1, 4, 1, 5]
// A trailing closure can be several statements long.
let sorted = values.sorted { first, second in
let firstIsOdd = first % 2 == 1
let secondIsOdd = second % 2 == 1
if firstIsOdd != secondIsOdd { return firstIsOdd }
return first < second
}
print(sorted)
// Capturing, and the shorthand argument names.
let threshold = 3
print(values.filter { $0 > threshold })import functools
values = [3, 1, 4, 1, 5]
# A lambda is one expression, so anything longer gets a name.
def odd_first(value):
return (value % 2 == 0, value)
print(sorted(values, key=odd_first))
threshold = 3
print([value for value in values if value > threshold])
# A decorator is a function that wraps a function — the shape that
# takes the place of a lot of closure passing.
def announced(function):
@functools.wraps(function)
def wrapper(*args, **keywords):
print(f"calling {function.__name__}")
return function(*args, **keywords)
return wrapper
@announced
def total(numbers):
return sum(numbers)
print(total(values))Two differences change how code is shaped. Python's
sorted takes a key — a function producing a sort key — rather than a two-argument comparator, which is both faster and impossible to get wrong; functools.cmp_to_key exists for the rare case where the comparison really is the only thing available. And a closure may read an enclosing variable but not rebind it without saying nonlocal, where Swift captures by reference by default. The decorator is the idiom worth taking away: @announced is exactly total = announced(total), and it is how logging, caching (@functools.cache) and registration are all written.Collections & Comprehensions
map and filter become a comprehension
A comprehension is
map and filter in one expression, and it is the idiomatic form rather than a clever alternative to one.let names = ["Ada", "Grace", "Alan", "Barbara"]
print(names.filter { $0.count > 3 }.map { $0.uppercased() })
print(names.map(\.count).reduce(0, +))
print(names.sorted())
print(names.first { $0.hasPrefix("G") } ?? "(none)")
var ages = ["Ada": 36, "Alan": 41]
ages["Grace"] = 45
print(ages["Ada"] ?? 0) // an Optional
print(ages["Nobody"] ?? 0)
let doubledAges = ages.mapValues { $0 * 2 }
print(doubledAges.count)names = ["Ada", "Grace", "Alan", "Barbara"]
# The comprehension is THE idiom: filter and map in one, and it reads left to right.
print([name.upper() for name in names if len(name) > 3])
print(sum(len(name) for name in names)) # a GENERATOR: lazy, no list built
print(sorted(names))
print(next((name for name in names if name.startswith("G")), "(none)"))
ages = {"Ada": 36, "Alan": 41}
ages["Grace"] = 45
print(ages["Ada"])
print(ages.get("Nobody", 0)) # [] on a missing key RAISES KeyError; .get does not
# Dict and set comprehensions, too.
doubled = {name: age * 2 for name, age in ages.items()}
print(len(doubled))
print({len(name) for name in names}) # a setThe comprehension replaces
map, filter, and often reduce, and it works for lists, dicts, and sets with the same syntax. Written with parentheses instead of brackets it becomes a generator — lazy, one element at a time, no intermediate list — which is Swift's .lazy. One difference with teeth: subscripting a dict with a missing key raises KeyError rather than returning an optional, so .get(key, default) is what you actually want (and collections.defaultdict when you want inserts too).The Method Names, Side by Side
Most of the vocabulary transfers directly; the difference is that Python's are usually free functions taking the collection rather than methods on it.
let values = [3, 1, 4, 1, 5]
print(values.sorted())
print(values.sorted(by: >))
print(values.first { $0 > 3 } ?? -1)
print(values.contains(4))
print(values.allSatisfy { $0 > 0 })
print(values.reduce(0, +))
print(Array(Set(values)).sorted())
print(values.enumerated().map { "\($0.offset):\($0.element)" }.joined(separator: " "))values = [3, 1, 4, 1, 5]
print(sorted(values))
print(sorted(values, reverse=True))
print(next((value for value in values if value > 3), -1))
print(4 in values)
print(all(value > 0 for value in values))
print(sum(values))
print(sorted(set(values)))
print(" ".join(f"{index}:{value}" for index, value in enumerate(values)))The pairs to memorize:
sorted() is sorted(values), contains is the in operator, allSatisfy is all, contains(where:) is any, reduce(0, +) is sum, compactMap is a comprehension with an if, flatMap is a nested comprehension or itertools.chain, and enumerated() is enumerate. 🚨 The one to watch is sorting: values.sorted() returns a new list and values.sort() sorts in place and returns None, so print(values.sort()) prints None — a mistake Swift makes impossible by naming the two differently.lazy Becomes a Generator
Swift's
.lazy is an opt-in that most code never uses. Python's generators are the same idea made ordinary — and several built-ins already return one.// Eager: each step builds a whole new array.
let squares = (1...5).map { $0 * $0 }.filter { $0 % 2 == 1 }
print(squares)
// Lazy: nothing runs until it is consumed.
let lazily = (1...1_000_000).lazy.map { $0 * $0 }.filter { $0 % 2 == 1 }
print(lazily.prefix(3).map { $0 })
// A sequence generated on demand needs a whole type.
struct Counter: Sequence, IteratorProtocol {
var current = 0
mutating func next() -> Int? {
current += 1
return current <= 3 ? current : nil
}
}
print(Array(Counter()))import itertools
# A comprehension with brackets is eager; with parentheses it is lazy.
squares = [value * value for value in range(1, 6) if (value * value) % 2 == 1]
print(squares)
lazily = (value * value for value in range(1, 1_000_001))
print(list(itertools.islice((v for v in lazily if v % 2 == 1), 3)))
# yield turns a function into a generator — no protocol, no struct.
def counter():
current = 1
while current <= 3:
yield current
current += 1
print(list(counter()))yield is the piece with no Swift counterpart at all: the function suspends at each yield, keeps its local variables, and resumes where it left off — so an iterator that would be a struct with a next() in Swift is three lines here. 🚨 A generator is consumed once, which is the surprise: iterating it a second time yields nothing at all, silently, where a Swift lazy sequence can be walked again. When you need both, keep the list. itertools is the toolbox for the rest — islice, chain, groupby, takewhile, count — and range, zip, map and enumerate are all lazy already.Classes & Dataclasses
struct becomes @dataclass
@dataclass writes the initializer, the equality and the description a Swift struct gets for free — and the result is still a reference type.struct Person: Equatable {
var name: String
var age: Int = 0
var isAdult: Bool { age >= 18 } // a computed property
func greeting() -> String { "Hi, \(name)" }
}
let ada = Person(name: "Ada", age: 36)
print(ada)
print(ada.isAdult, ada.greeting())
print(ada == Person(name: "Ada", age: 36)) // structural equality, free
// A copy, with one field changed.
var older = ada
older.age = 37
print(older)from dataclasses import dataclass, replace, field
@dataclass
class Person:
name: str
age: int = 0
# A computed property: a method with @property, called without parentheses.
@property
def is_adult(self) -> bool:
return self.age >= 18
def greeting(self) -> str:
return f"Hi, {self.name}"
ada = Person(name="Ada", age=36)
print(ada) # __repr__, generated
print(ada.is_adult, ada.greeting())
print(ada == Person("Ada", 36)) # __eq__, generated
# replace() is Swift's copy-with-changes. Plain assignment would ALIAS.
older = replace(ada, age=37)
print(older, ada)
# @dataclass(frozen=True) makes it immutable and hashable — as close to a Swift
# struct as Python gets. Reach for it by default.
@dataclass(frozen=True)
class Point:
x: int
y: int
point = Point(1, 2)
print({point}) # hashable, so it works in a set
# point.x = 9 ← raises FrozenInstanceError@dataclass generates __init__, __repr__ and __eq__ from the annotated fields, which covers most of what a Swift struct gives you — but it is still a class, so assignment aliases and replace(instance, field=value) is how you produce a modified copy. The closest thing to real value semantics is @dataclass(frozen=True): immutable, hashable, and safe to share, and it is worth making your default. Note also @property, which turns a method into a computed property accessed without parentheses — the direct translation of Swift's var isAdult: Bool { … }.Classes, self, and no access control
The shape is familiar and two things are not:
self is an explicit first parameter, and there is no private anywhere in the language.class Counter {
private var count = 0 // genuinely private
static let maximum = 100
func increment() {
count += 1
}
var value: Int { count }
deinit { print("gone") } // deterministic, via ARC
}
let counter = Counter()
counter.increment()
counter.increment()
print(counter.value, Counter.maximum)
// counter.count ← will not compile: privateclass Counter:
maximum = 100 # a class attribute: Swift's 'static'
def __init__(self):
# A leading underscore means "private" BY CONVENTION. Nothing enforces it:
# counter._count is perfectly legal, and linters will only tut at you.
self._count = 0
def increment(self) -> None: # 'self' is EXPLICIT in every method signature
self._count += 1
@property
def value(self) -> int:
return self._count
counter = Counter()
counter.increment()
counter.increment()
print(counter.value, Counter.maximum)
print(counter._count) # reachable. Python trusts you.
# And you can add an attribute that was never declared, at any time.
counter.surprise = "hello"
print(counter.surprise)Three things to absorb.
self is an explicit first parameter of every method — Python does not hide it. There is no access control: a leading underscore is a convention that says "do not touch", and nothing at all prevents you from touching it (a double underscore triggers name mangling, which is obfuscation rather than privacy). And objects are open — you can attach a brand-new attribute to an instance at run time, which is powerful, occasionally useful, and the reason a typo becomes a silently created field rather than an error.Computed Properties Become @property
A computed property is a method that a decorator makes look like an attribute, which is exactly what Swift's braces do — and it is how you add validation to something that was a plain attribute yesterday.
struct Rectangle {
var width: Double
var height: Double
var area: Double { width * height }
var diagonal: Double {
get { (width * width + height * height).squareRoot() }
}
var scale: Double = 1 {
didSet { print("scale is now \(scale)") }
}
}
var rectangle = Rectangle(width: 3, height: 4)
print(rectangle.area, rectangle.diagonal)
rectangle.scale = 2import math
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
self._scale = 1.0
@property
def area(self):
return self.width * self.height
@property
def diagonal(self):
return math.hypot(self.width, self.height)
@property
def scale(self):
return self._scale
@scale.setter
def scale(self, value):
# There is no didSet; a setter is where the observation goes.
self._scale = value
print(f"scale is now {value}")
rectangle = Rectangle(3.0, 4.0)
print(rectangle.area, rectangle.diagonal)
rectangle.scale = 2.0The real value of
@property is that it is retrofittable: rectangle.width can start life as a plain attribute and become a validated property later without a single caller changing, which is why Python code does not start with getters and setters the way other languages' does. The leading underscore on _scale is the whole of the privacy story — a convention, enforced by nothing. What is genuinely missing is willSet, lazy var (though functools.cached_property covers the common case) and property wrappers.Duck Typing & Protocols
Protocols become duck typing
A protocol says what a type must provide and the compiler checks it. Duck typing asks the same question at the moment of the call, and nothing checks it before then.
protocol Describable {
func describe() -> String
}
// Conformance is DECLARED, and the compiler checks it.
struct Dog: Describable {
func describe() -> String { "a dog" }
}
struct Robot: Describable {
func describe() -> String { "BEEP" }
}
func announce(_ value: any Describable) {
print(value.describe())
}
announce(Dog())
announce(Robot())
// A type that does not conform CANNOT be passed. Checked at compile time.# No protocol, no conformance, no declaration. If it has the method, it works.
class Dog:
def describe(self) -> str:
return "a dog"
class Robot:
def describe(self) -> str:
return "BEEP"
def announce(value) -> None:
print(value.describe()) # nothing checked this until it ran
announce(Dog())
announce(Robot())
# Pass something without the method and it fails HERE, at run time.
try:
announce(42)
except AttributeError as error:
print("AttributeError:", error)
# typing.Protocol brings STRUCTURAL typing back for the type checker (mypy) —
# still no conformance declaration, but now a static tool verifies the shape.
from typing import Protocol
class Describable(Protocol):
def describe(self) -> str: ...
def announce_checked(value: Describable) -> None:
print(value.describe())
announce_checked(Dog()) # mypy verifies Dog has describe(); Python does notDuck typing is the whole story: a function accepts anything with the right method, no conformance is declared, and the check happens when the call runs. That buys enormous flexibility (any object can stand in for any other, which is why mocking in Python is trivial) and costs you the compiler —
announce(42) is a perfectly good program until the moment it is not. typing.Protocol is the bridge: it describes the shape you need, and mypy verifies it statically, giving you back something close to a Swift protocol with structural rather than nominal conformance. Python still does not check it at run time.Operator overloading is a dunder method
Swift overloads an operator by writing a
static func named after it. Python implements a method with a reserved name, and every operator has one.struct Money: CustomStringConvertible, Equatable, Comparable {
let cents: Int
var description: String { "$\(Double(cents) / 100)" }
static func + (left: Money, right: Money) -> Money {
Money(cents: left.cents + right.cents)
}
static func < (left: Money, right: Money) -> Bool {
left.cents < right.cents
}
}
let total = Money(cents: 500) + Money(cents: 250)
print(total)
print(Money(cents: 100) < total)
print([Money(cents: 300), Money(cents: 100)].sorted().map(\.description))from dataclasses import dataclass
from functools import total_ordering
@total_ordering # generates <=, >, >= from __eq__ and __lt__
@dataclass(frozen=True)
class Money:
cents: int
def __str__(self) -> str: # CustomStringConvertible
return f"${self.cents / 100}"
def __add__(self, other: "Money") -> "Money": # the + operator
return Money(self.cents + other.cents)
def __lt__(self, other: "Money") -> bool: # the < operator
return self.cents < other.cents
total = Money(500) + Money(250)
print(total)
print(Money(100) < total)
print([str(money) for money in sorted([Money(300), Money(100)])])
# The protocol you implement is the DUNDER method: __len__, __iter__, __eq__,
# __getitem__, __contains__ — the whole language is built on them, and
# implementing them makes your class work with len(), for, in, and [].Every operator and every built-in function is a "dunder" (double-underscore) method:
+ calls __add__, len(x) calls x.__len__(), for x in thing calls __iter__, thing[0] calls __getitem__. That is Python's answer to Swift's standard-library protocols (Equatable, Comparable, Collection), and it is the whole language in one idea — implement the right dunder and your type works with the built-in syntax. Nothing declares that you conform to anything; you just define the method.When Duck Typing Is Not Enough
Duck typing checks at the call. An abstract base class checks at construction, which is the closest Python gets to the compiler refusing to build a type that forgot a method.
protocol Shape {
func area() -> Double
func name() -> String
}
extension Shape {
func describe() -> String { "\(name()): \(area())" } // default
}
struct Circle: Shape {
let radius: Double
func area() -> Double { 3.14159 * radius * radius }
func name() -> String { "circle" }
}
// struct Broken: Shape {} — will not compile: missing requirements.
print(Circle(radius: 1).describe())from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
@abstractmethod
def name(self) -> str: ...
def describe(self) -> str: # a default implementation
return f"{self.name()}: {self.area()}"
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius * self.radius
def name(self):
return "circle"
print(Circle(1).describe())
class Broken(Shape):
pass
try:
Broken()
except TypeError as error:
print(type(error).__name__) # at construction, not at compile timeThe three tools sit on a line. Duck typing checks nothing until the call.
typing.Protocol from the previous section checks structurally, in a type checker, with no inheritance — closest to what a Swift protocol feels like. ABC checks at run time, at the moment you try to instantiate, and requires the subclass to say it inherits. Choose Protocol when the implementations are not yours to change, and ABC when you own the hierarchy and want the failure loud. Neither gives you Swift's guarantee, which is that the program does not build at all.Enums & Pattern Matching
switch becomes match — without exhaustiveness
Python 3.10 added structural pattern matching, and it is closer to Swift's
switch than you would expect — with one guarantee missing.enum Shape {
case circle(radius: Double)
case rectangle(width: Double, height: Double)
}
func area(_ shape: Shape) -> Double {
switch shape { // EXHAUSTIVE: a missing case will not compile
case .circle(let radius):
return 3.14159 * radius * radius
case .rectangle(let width, let height):
return width * height
}
}
print(area(.circle(radius: 1)))
print(area(.rectangle(width: 2, height: 3)))from dataclasses import dataclass
@dataclass
class Circle:
radius: float
@dataclass
class Rectangle:
width: float
height: float
def area(shape) -> float:
match shape:
case Circle(radius=radius): # destructures as it matches
return 3.14159 * radius * radius
case Rectangle(width=width, height=height):
return width * height
case _: # NOT optional: nothing checks
return 0.0 # that you covered every case
print(area(Circle(1)))
print(area(Rectangle(2, 3)))
# match also destructures dicts and lists, which Swift cannot do:
message = {"type": "click", "x": 3, "y": 4}
match message:
case {"type": "click", "x": x, "y": y}:
print(f"click at {x},{y}")
case {"type": "key", "key": key}:
print(f"key {key}")
match [1, 2, 3, 4]:
case [first, *rest]:
print(first, rest)match is genuinely good: it destructures classes as it matches (case Circle(radius=radius)), it takes guards (if), and it goes further than Swift by matching the shape of dicts and lists — which is exactly what you want when handling decoded JSON. What it does not have is exhaustiveness: nothing tells you a case is missing, so case _ is doing real work, and adding a third shape silently falls into it. That guarantee — the one that makes Swift enums so safe to extend — has no Python equivalent, though a strict mypy setup can approximate it with assert_never.Simple enums
Both languages have an enum whose cases are names rather than integers, and Python's is a class in the standard library rather than a keyword.
enum Status: String, CaseIterable {
case active = "running"
case paused = "on hold"
var isRunning: Bool { self == .active }
}
for status in Status.allCases {
print(status, status.rawValue, status.isRunning)
}
print(Status(rawValue: "running") ?? .paused)from enum import Enum
class Status(Enum):
ACTIVE = "running" # the value IS the rawValue
PAUSED = "on hold"
@property
def is_running(self) -> bool:
return self is Status.ACTIVE
for status in Status: # iterable: allCases, free
print(status.name, status.value, status.is_running)
print(Status("running")) # construct from the value — raises ValueError on a miss
print(Status.ACTIVE is Status("running")) # enum members are singletons: use 'is'Python enums carry values (the
rawValue), are iterable without asking (allCases for free), and can hold properties and methods. Two notes: constructing from a value raises ValueError on a miss rather than returning an optional, and members are singletons, so is is the idiomatic comparison. What Python's Enum cannot do is carry per-case associated values — for that you are back to the dataclasses-plus-match pattern in the previous concept.🚨 No Associated Values
🚨 The largest single loss on the page. A Python
Enum member carries one fixed value, so an enum whose cases hold different payloads has to be rebuilt as a class hierarchy.enum Shape {
case circle(radius: Double)
case rectangle(width: Double, height: Double)
}
func area(_ shape: Shape) -> Double {
switch shape { // exhaustive: adding a case breaks the build
case .circle(let radius): return 3.14159 * radius * radius
case .rectangle(let width, let height): return width * height
}
}
print(area(.rectangle(width: 2, height: 3)))
print(area(.circle(radius: 1)))from dataclasses import dataclass
@dataclass
class Circle:
radius: float
@dataclass
class Rectangle:
width: float
height: float
def area(shape):
match shape:
case Circle(radius=r):
return 3.14159 * r * r
case Rectangle(width=w, height=h):
return w * h
case _:
raise ValueError(f"unhandled: {shape}")
print(area(Rectangle(2.0, 3.0)))
print(area(Circle(1.0)))Dataclasses plus
match get the shape back, and the guarantee does not come with them: the case _ arm exists because nothing can prove the list is complete, so adding a third shape is a run-time ValueError rather than a build failure. That discard arm is the idiom precisely because it turns a silent wrong answer into a loud one — leaving it out makes match fall through and return None. A type checker can be told to help with typing.assert_never in the final arm, which is as close to exhaustiveness as Python offers.Errors & Exceptions
throws disappears; exceptions are everywhere
🚨 There is no
throws in a signature and no try at a call site. Any function may raise anything, and nothing declares it.enum ParseError: Error {
case notANumber(String)
case notPositive(Int)
}
// 'throws' is in the signature; 'try' is required at every call site.
func parsePositive(_ text: String) throws -> Int {
guard let number = Int(text) else { throw ParseError.notANumber(text) }
guard number > 0 else { throw ParseError.notPositive(number) }
return number
}
do {
print(try parsePositive("42"))
print(try parsePositive("-1"))
} catch ParseError.notPositive(let number) {
print("not positive: \(number)")
} catch {
print("failed: \(error)")
}class NotANumber(Exception):
pass
class NotPositive(Exception):
def __init__(self, number: int):
super().__init__(f"not positive: {number}")
self.number = number
# No 'throws' in the signature and no 'try' at the call site. Nothing tells a
# caller this can fail — and in Python, almost everything can.
def parse_positive(text: str) -> int:
try:
number = int(text)
except ValueError:
raise NotANumber(text) from None
if number <= 0:
raise NotPositive(number)
return number
try:
print(parse_positive("42"))
print(parse_positive("-1"))
except NotPositive as error:
print("not positive:", error.number)
except Exception as error:
print("failed:", error)
finally:
print("done")
# EAFP — "easier to ask forgiveness than permission" — is the house style:
# try the thing, and catch the failure, rather than checking first.
try:
value = {"a": 1}["b"]
except KeyError:
value = 0
print(value)The
throws annotation is gone, so a function's signature no longer tells you it can fail — and in Python that is a bigger deal than it sounds, because exceptions are used for ordinary control flow: a missing dict key raises, a failed int() raises, and iteration itself ends with a StopIteration. The cultural name for this is EAFP ("easier to ask forgiveness than permission"): the idiomatic move is to attempt the operation and catch the failure, where a Swift developer's instinct is to check first. Both languages have finally, and neither has checked exceptions.defer becomes a context manager
defer registers cleanup at the point you acquire something. A with block ties it to the thing itself, so a caller cannot forget it.func process() {
print("open")
defer { print("close") } // runs on EVERY exit path
print("work")
}
process()
// defer is scoped and general: it can run any code on the way out.
func guarded() {
var count = 0
defer { print("count ended at \(count)") }
count += 1
}
guarded()# 'with' is the cleanup construct, and it is tied to a RESOURCE rather than a
# scope: the object's __enter__ / __exit__ run on entry and on every exit path.
class Resource:
def __enter__(self):
print("open")
return self
def __exit__(self, exception_type, exception, traceback):
print("close") # runs even if the body raised
return False # False = do not swallow the exception
with Resource() as resource:
print("work")
# The everyday use, and why you will type 'with' all day:
# with open("file.txt") as handle:
# handle.read() ← the file is closed for you, exception or not
# contextlib turns a generator into a context manager — the closest thing to a
# general-purpose defer.
from contextlib import contextmanager
@contextmanager
def timing(label: str):
print(f"start {label}")
try:
yield
finally:
print(f"end {label}")
with timing("work"):
print("doing it")Python has no
defer. Cleanup is attached to a resource rather than a scope: with resource: calls __enter__ on entry and __exit__ on every exit path, including an exception — which is why with open(…) is how every file in Python is opened. For the general case (run this code on the way out, whatever happens), @contextmanager turns a generator into one: everything before the yield is the entry, everything in the finally is the exit. It is more ceremony than defer for a one-off, and better than defer when the cleanup belongs to a thing rather than to a block.Catching by Type, and the else Clause
Swift catches by matching an error value. Python catches by class, so one
except can take a whole family — and there are two clauses on the statement that Swift has no equivalent for.enum ConfigurationError: Error {
case missing(String)
case outOfRange(Int)
}
func port(_ text: String) throws -> Int {
guard let value = Int(text) else { throw ConfigurationError.missing(text) }
guard (1...65535).contains(value) else { throw ConfigurationError.outOfRange(value) }
return value
}
for text in ["8080", "abc", "99999"] {
do {
print(try port(text))
} catch ConfigurationError.missing(let raw) {
print("not a number: \(raw)")
} catch {
print("other: \(error)")
}
}class ConfigurationError(ValueError):
pass
class Missing(ConfigurationError):
pass
class OutOfRange(ConfigurationError):
pass
def port(text):
try:
value = int(text)
except ValueError:
raise Missing(f"not a number: {text}") from None
if not 1 <= value <= 65535:
raise OutOfRange(f"{value} is outside 1..65535")
return value
for text in ["8080", "abc", "99999"]:
try:
value = port(text)
except Missing as error:
print(error)
except ConfigurationError as error: # catches OutOfRange too
print(f"other: {error}")
else:
print(value) # only when nothing was raised
finally:
pass # always, like deferThe hierarchy is what a Swift enum of errors cannot do:
except ConfigurationError catches every subclass, except ValueError catches those plus anything else meaning "bad value", and a caller that wants to distinguish them still can. The else clause is the underused one — it runs only when the try block raised nothing, which keeps the success path out of the block being guarded, so an exception raised by print(value) is not accidentally caught by the handler above it. raise … from None suppresses the chained ValueError; from error keeps it when the cause is worth showing.Concurrency & the GIL
async/await, and the GIL underneath
The syntax is the same. The execution model is not: Python's threads cannot run your code in parallel, and this fact organizes everything about concurrency in the language.
import Foundation
func fetch(_ id: Int) async -> String {
try? await Task.sleep(for: .milliseconds(10))
return "user-\(id)"
}
// Swift's task group runs on a real thread pool: this is genuinely parallel,
// and CPU-bound work in a task uses another core.
func fetchAll() async -> [String] {
await withTaskGroup(of: String.self) { group in
for id in 1...3 {
group.addTask { await fetch(id) }
}
var results: [String] = []
for await result in group {
results.append(result)
}
return results.sorted()
}
}
print(await fetchAll())import asyncio
async def fetch(id: int) -> str:
await asyncio.sleep(0.01) # yields to the event loop; does not block it
return f"user-{id}"
async def fetch_all() -> list[str]:
# gather is awaitAll. It runs the coroutines CONCURRENTLY on ONE thread —
# they interleave at each await, and none of them runs in parallel.
results = await asyncio.gather(*(fetch(id) for id in range(1, 4)))
return sorted(results)
async def main() -> None:
print(await fetch(1))
print(await fetch_all())
asyncio.run(main())
# The GIL: only ONE thread executes Python bytecode at a time. So:
# - I/O-bound work → asyncio (or threads): fine, because waiting releases the GIL
# - CPU-bound work → multiprocessing: a separate PROCESS per core, with the
# arguments pickled across the boundary
# Swift's task group parallelizes CPU work for free. Python's does not, and
# reaching for threads to speed up computation will make it SLOWER.The syntax ports one-for-one —
async def, await, and asyncio.gather for awaitAll — and then the Global Interpreter Lock changes what it all means. Only one thread runs Python bytecode at a time, so asyncio gives you concurrency (thousands of overlapping I/O waits on a single thread) but never parallelism. CPU-bound work needs multiprocessing, which forks real processes and pickles the arguments across the boundary — closer to Elixir's isolation than to Swift's task group. The rule of thumb: asyncio for waiting, multiprocessing for computing, and threads almost never. (The GIL is finally becoming optional in 3.13+, but assume it is there.)🚨 Threads Do Not Give You Parallelism
🚨 The single most important thing to know before writing concurrent Python. One lock lets exactly one thread execute bytecode at a time, so threads help with waiting and not with computing.
// Swift threads are real: this uses every core.
//
// import Foundation
//
// let queue = DispatchQueue.global(attributes: .concurrent)
// DispatchQueue.concurrentPerform(iterations: 4) { index in
// var total = 0
// for value in 0..<10_000_000 { total &+= value }
// print(index, total)
// }
//
// And an actor serializes access to its own state, with the compiler
// checking that nothing reaches it unsafely.
print("real threads, checked by the compiler")# Illustrative: this starts operating-system threads and a process pool.
#
# import threading, multiprocessing
#
# def work(index):
# total = sum(range(10_000_000))
# print(index, total)
#
# # Four threads. The GIL means this is NOT four times faster —
# # it is slightly slower than doing it once, four times.
# threads = [threading.Thread(target=work, args=(i,)) for i in range(4)]
# for thread in threads: thread.start()
# for thread in threads: thread.join()
#
# # Four PROCESSES. Real parallelism, at the cost of pickling
# # everything that crosses between them.
# with multiprocessing.Pool(4) as pool:
# pool.map(work, range(4))
print("threads for waiting, processes for computing")The decision procedure is short. Work that waits — network, disk, a subprocess — releases the lock while it waits, so
threading and asyncio both help, and asyncio scales further because a coroutine is far cheaper than a thread. Work that computes needs separate interpreters: multiprocessing, concurrent.futures.ProcessPoolExecutor, or pushing the loop into C by way of numpy. Python 3.13 added an experimental free-threaded build with no lock at all, and 3.14 made per-interpreter subinterpreters available from Python — both real, neither yet the default, so the rule above is still the one to plan around. Nothing here corresponds to a Swift actor: shared state is protected by a threading.Lock you remember to take.