Output & Running
Hello, World
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
} print("Hello, World!") Python needs no package clause, no
import, and no main function — top-level code runs the moment the file is loaded. print is a built-in function (not a statement) and adds a newline like fmt.Println. There is no compile step at all: the interpreter parses and runs the script directly.Running a program
// Compile and run in one step:
// go run main.go
// Build a standalone binary:
// go build -o app main.go && ./app # Run directly (no build step):
# python3 hello.py
#
# One-liner without a file:
# python3 -c 'print("Hi")'
#
# Interactive REPL:
# python3 Python is interpreted: there is no
go build and no compiled binary to ship — the Python interpreter must be present wherever the script runs. python3 file.py runs in one step, and the bare python3 command opens a REPL for quick experiments, which Go has no built-in equivalent for.Formatted output
package main
import "fmt"
func main() {
name := "Python"
version := 3.13
fmt.Printf("%s %.2f\n", name, version)
} name = "Python"
version = 3.13
print(f"{name} {version:.2f}") Python's f-strings interpolate any expression inline:
f"{name} {version:.2f}" embeds variables and applies a format spec after the colon. It replaces Go's Printf verbs, and because print already adds a newline you rarely write \n. The format mini-language (:.2f, :>10, :,) covers what Go's % verbs do.Comments
package main
import "fmt"
func main() {
// Single-line comment
/* Block comment */
count := 0
fmt.Println(count)
} # Single-line comment
"""
A triple-quoted string used as a
multi-line comment / docstring.
"""
count = 0
print(count) Python uses
# for comments and has no /* ... */ block form. For multi-line notes the convention is a triple-quoted string, which doubles as a docstring when placed at the top of a module, function, or class — accessible at runtime via __doc__, something Go comments cannot do.Variables & Types
Declaring variables
package main
import "fmt"
func main() {
count := 42
name := "Python"
ready := true
fmt.Println(count, name, ready)
} count = 42
name = "Python"
ready = True
print(count, name, ready) Python has no
:=, no var, and no type annotations are required — a variable exists once you assign to it and can later hold a value of any type. There is no "declared but not used" error as in Go, so an unused variable is legal. Note the capitalised True/False/None.None vs zero values
package main
import "fmt"
func main() {
var count int // 0
var label string // ""
var ready bool // false
fmt.Printf("%d %q %t\n", count, label, ready)
} count = None
label = None
ready = None
print(count, label, ready) Go gives every declared variable a typed zero value. Python has a single
None for "no value" and no concept of an uninitialised-but-typed variable — reading a name you never assigned raises NameError rather than yielding a zero. There is no implicit empty string or zero integer; you assign one explicitly if you want it.Dynamic typing
package main
import "fmt"
func main() {
// Go: a variable's type is fixed at declaration.
var value int = 42
// value = "now a string" // compile error
fmt.Println(value)
} value = 42
print(type(value).__name__)
value = "now a string" # totally fine — names are not typed
print(type(value).__name__) In Go a variable has one type for its whole life. In Python a name is just a binding to an object, so the same name can refer to an
int and later a str. Types live on the values, not the names — type(value) inspects the object. Optional type hints (value: int = 42) document intent but are not enforced at runtime.Multiple assignment & swap
package main
import "fmt"
func main() {
first, second := 1, 2
first, second = second, first // swap
fmt.Println(first, second)
} first, second = 1, 2
first, second = second, first # swap
print(first, second)
# Unpack with a catch-all:
head, *rest = [1, 2, 3, 4]
print(head, rest) Both languages support parallel assignment and the tuple swap
first, second = second, first with no temporary. Python goes further with unpacking: head, *rest = [...] binds the first element and gathers the remainder into a list, and the same star works on the right-hand side to spread an iterable into arguments.Strings
String basics
package main
import "fmt"
func main() {
language := "Python"
fmt.Println(len(language))
fmt.Println(language + " rocks")
} language = "Python"
print(len(language))
print(language + " rocks")
print(language * 2) # repetition Python strings are immutable objects with many methods, and
len() counts characters, not bytes (a Python str is a sequence of Unicode code points, unlike Go's byte-oriented string). The + operator concatenates and * repeats — "ab" * 3 is "ababab", which Go has no operator for.f-strings vs Sprintf
package main
import "fmt"
func main() {
name := "World"
count := 3
message := fmt.Sprintf("Hello, %s! x%d", name, count)
fmt.Println(message)
} name = "World"
count = 3
message = f"Hello, {name}! x{count}"
print(message)
print(f"{count * 2 = }") # self-documenting: count * 2 = 6 An f-string interpolates any expression inside
{...}, so f"{name}" replaces Go's positional Sprintf verbs entirely. Format specs come after a colon ({value:.2f}), and the trailing-= form f"{count * 2 = }" prints both the expression and its value — handy for debugging.String methods
package main
import (
"fmt"
"strings"
)
func main() {
sentence := "the quick brown fox"
fmt.Println(strings.ToUpper(sentence))
fmt.Println(strings.Split(sentence, " "))
fmt.Println(strings.Contains(sentence, "quick"))
} sentence = "the quick brown fox"
print(sentence.upper())
print(sentence.split(" "))
print("quick" in sentence)
print(sentence.replace("quick", "slow")) Python calls these as methods on the string itself rather than passing it to a
strings package function. Membership uses the in operator — "quick" in sentence — instead of a Contains call, and in works the same way for lists, dicts, and sets, making it a general containment test.Slicing with negative indices
package main
import "fmt"
func main() {
text := "Python"
fmt.Println(text[0:3]) // "Pyt"
fmt.Println(text[len(text)-1:]) // last char
} text = "Python"
print(text[0:3]) # "Pyt"
print(text[-1]) # "n" — negative indexes from the end
print(text[::-1]) # "nohtyP" — reversed via step Python slicing is far richer than Go's. Negative indices count from the end (
text[-1] is the last character), and the full form text[start:stop:step] adds a step, so text[::-1] reverses a sequence. The same slice syntax works on lists and tuples, and going out of bounds yields an empty slice instead of panicking.Numbers
Arbitrary-precision integers
package main
import (
"fmt"
"math/big"
)
func main() {
result := new(big.Int).Exp(big.NewInt(2), big.NewInt(100), nil)
fmt.Println(result)
} result = 2 ** 100
print(result)
print(type(result).__name__) A Go
int is a fixed 64-bit value that overflows silently, so large integers need the math/big package and its method-based arithmetic. Python's int is arbitrary precision by default — 2 ** 100 just works and stays an ordinary int, with the exponent operator ** built into the language.Division operators
package main
import "fmt"
func main() {
fmt.Println(7 / 2) // 3 (integer division)
fmt.Println(7.0 / 2.0) // 3.5
fmt.Println(7 % 2) // 1
} print(7 / 2) # 3.5 — / is always float division
print(7 // 2) # 3 — // is floor division
print(7 % 2) # 1
print(2 ** 10) # 1024 — exponent A surprise for Go programmers: Python's
/ always produces a float, even for two integers, so 7 / 2 is 3.5. Integer (floor) division is the separate // operator. Python also has the built-in exponent operator **, where Go requires math.Pow and a float result.No overflow, no sized ints
package main
import "fmt"
func main() {
var value int8 = 127
value++ // wraps silently to -128
fmt.Println(value)
} value = 127
value += 1
print(value) # 128 — Python ints never overflow or wrap
big = 10 ** 50
print(big + 1) # grows without limit Go has sized integer types (
int8, uint64) that wrap on overflow. Python has a single int type with no fixed width and no overflow — it simply grows to hold any value. The cost is that Python numbers are heap-allocated objects rather than machine words, so heavy numeric code reaches for libraries like NumPy.Lists & Slices
Lists vs slices
package main
import "fmt"
func main() {
numbers := []int{1, 2, 3}
numbers = append(numbers, 4)
fmt.Println(numbers, len(numbers))
} numbers = [1, 2, 3]
numbers.append(4)
print(numbers, len(numbers))
numbers.extend([5, 6]) # append several
print(numbers) A Python
list is the everyday growable sequence, like a Go slice, but it can hold mixed types and grows in place with append (no reassignment as Go's append needs). len() is a built-in function rather than a method, and there is no separate fixed-array vs slice distinction — one type does both.Iterating
package main
import "fmt"
func main() {
fruits := []string{"apple", "banana"}
for index, fruit := range fruits {
fmt.Printf("%d: %s\n", index, fruit)
}
} fruits = ["apple", "banana"]
for index, fruit in enumerate(fruits):
print(f"{index}: {fruit}") Python's
for iterates the elements directly, and enumerate() pairs each with its index — the equivalent of Go's for index, value := range. To loop a fixed number of times use for index in range(n). There is no C-style counter loop; iteration is always over an iterable.Sorting
package main
import (
"fmt"
"sort"
)
func main() {
numbers := []int{3, 1, 4, 1, 5}
sort.Ints(numbers)
fmt.Println(numbers)
words := []string{"banana", "apple", "cherry"}
sort.Slice(words, func(left, right int) bool {
return len(words[left]) < len(words[right])
})
fmt.Println(words)
} numbers = [3, 1, 4, 1, 5]
print(sorted(numbers)) # returns a new list
words = ["banana", "apple", "cherry"]
print(sorted(words, key=len)) # sort by a key function The built-in
sorted() returns a new sorted list (use list.sort() to sort in place). A custom order is given by a key function that maps each element to its sort key — key=len sorts by length — which is terser than Go's comparator and, like Go's sort.Slice, computes the key per element.Tuples
package main
import "fmt"
func main() {
// Go has no tuple type; use a struct or multiple values.
point := struct{ X, Y int }{3, 4}
fmt.Println(point.X, point.Y)
} point = (3, 4) # an immutable tuple
print(point[0], point[1])
x, y = point # unpack
print(x, y) A Python
tuple is an immutable, ordered group of values written with parentheses — there is no Go equivalent, which uses an anonymous struct or multiple return values instead. Tuples are commonly used for fixed records and as dict keys (lists cannot be keys because they are mutable), and they unpack just like multiple assignment.Dicts & Maps
Dicts vs maps
package main
import "fmt"
func main() {
ages := map[string]int{"Alice": 30}
ages["Bob"] = 25
delete(ages, "Alice")
fmt.Println(ages["Bob"], len(ages))
} ages = {"Alice": 30}
ages["Bob"] = 25
del ages["Alice"]
print(ages["Bob"], len(ages)) A Python
dict is the counterpart to a Go map, written with braces and colons. Since Python 3.7 a dict preserves insertion order when iterated, whereas Go deliberately randomises map order. Removal uses the del statement rather than a delete built-in, and keys may be any hashable value, not one declared type.Missing keys
package main
import "fmt"
func main() {
inventory := map[string]int{"apples": 5}
count, ok := inventory["bananas"]
if !ok {
fmt.Println("not stocked")
}
fmt.Println(count) // 0
} inventory = {"apples": 5}
print(inventory.get("bananas", 0)) # default if absent
if "bananas" not in inventory:
print("not stocked") Indexing a missing key with
inventory["bananas"] raises KeyError in Python, unlike Go's comma-ok which returns a zero value. The idiomatic way to supply a fallback is dict.get(key, default), and the in operator tests membership — replacing Go's value, ok := m[key] with two clearer, single-purpose tools.Iterating a dict
package main
import "fmt"
func main() {
scores := map[string]int{"math": 90, "art": 85}
for subject, score := range scores {
fmt.Printf("%s: %d\n", subject, score)
}
} scores = {"math": 90, "art": 85}
for subject, score in scores.items():
print(f"{subject}: {score}")
print(list(scores.keys()))
print(list(scores.values())) Iterating a dict directly yields its keys, so to get key and value together you call
.items() — the parallel to Go's range over a map, but explicit. .keys() and .values() return live views you can wrap in list(). The iteration order is the insertion order, which Go does not guarantee.Control Flow
Conditionals
package main
import "fmt"
func main() {
score := 85
if score >= 90 {
fmt.Println("A")
} else if score >= 80 {
fmt.Println("B")
} else {
fmt.Println("C")
}
} score = 85
if score >= 90:
print("A")
elif score >= 80:
print("B")
else:
print("C") Python uses indentation and a colon instead of braces, and the keyword is
elif (not else if). There are no parentheses around the condition. Indentation is significant: the block is defined by its level, so consistent spacing is part of the syntax rather than a style choice as in Go.Truthiness
package main
import "fmt"
func main() {
names := []string{}
// Go: only a bool is allowed in a condition.
if len(names) == 0 {
fmt.Println("empty")
}
} names = []
if not names: # empty containers are falsy
print("empty")
value = 0
print("zero is falsy" if not value else "truthy") Go permits only a genuine
bool in a condition. Python treats many values as "falsy" — 0, "", None, and empty containers ([], {}) all count as false — so if not names idiomatically tests for an empty list. This is concise but a trap for Go programmers used to explicit comparisons.match vs switch
package main
import "fmt"
func main() {
command := "start"
switch command {
case "start", "go":
fmt.Println("starting")
default:
fmt.Println("unknown")
}
} command = "start"
match command:
case "start" | "go":
print("starting")
case _:
print("unknown") Python's
match (added in 3.10) is the analogue of Go's switch but is a structural pattern match: "start" | "go" is an or-pattern, case _ is the wildcard, and patterns can also destructure lists, tuples, and objects by shape. For a simple value test it reads much like Go's comma-separated cases, with no fall-through.while and range
package main
import "fmt"
func main() {
for index := 0; index < 3; index++ {
fmt.Println(index)
}
count := 0
for count < 3 {
count++
}
fmt.Println("count:", count)
} for index in range(3):
print(index)
count = 0
while count < 3:
count += 1
print("count:", count) Python keeps a distinct
while (which Go folds into for) and uses for x in range(n) for counting loops — range(3) is a lazy sequence 0, 1, 2. Python loops also support an else clause that runs only if the loop finished without break, a feature Go has no equivalent for.Functions
Defining functions
package main
import "fmt"
func greet(name string) string {
return "Hello, " + name
}
func main() {
fmt.Println(greet("Python"))
} def greet(name):
return f"Hello, {name}"
print(greet("Python")) A Python
def needs no parameter types and no return type. The body is an indented block, and a function with no return implicitly returns None. Type hints (def greet(name: str) -> str:) are optional documentation that tools check but the interpreter ignores at runtime.Default & keyword arguments
package main
import "fmt"
// Go has neither default nor keyword arguments.
func connect(host string, port int, secure bool) string {
return fmt.Sprintf("%s:%d secure=%t", host, port, secure)
}
func main() {
fmt.Println(connect("localhost", 5432, true))
} def connect(host, port=5432, secure=False):
return f"{host}:{port} secure={secure}"
print(connect("localhost", secure=True)) Python supports default values and named keyword arguments, both of which Go omits. A parameter with a default (
port=5432) is optional, and callers may pass any argument by name in any order (secure=True), which removes the long positional argument lists Go forces. Beware: a mutable default like [] is shared across calls — use None and create it inside.Variadic: *args and **kwargs
package main
import "fmt"
func sum(numbers ...int) int {
total := 0
for _, number := range numbers {
total += number
}
return total
}
func main() {
fmt.Println(sum(1, 2, 3, 4))
} def sum_all(*numbers):
return sum(numbers)
print(sum_all(1, 2, 3, 4))
def describe(**fields):
return ", ".join(f"{key}={value}" for key, value in fields.items())
print(describe(host="local", port=8080)) Python's
*numbers gathers extra positional arguments into a tuple — the analogue of Go's ...int. It also has **fields, which collects arbitrary keyword arguments into a dict, something Go has no equivalent for. Both stars also work at the call site to spread a list or dict into arguments: sum_all(*[1, 2, 3]).Multiple return values
package main
import "fmt"
func minMax(numbers []int) (int, int) {
low, high := numbers[0], numbers[0]
for _, value := range numbers {
if value < low {
low = value
}
if value > high {
high = value
}
}
return low, high
}
func main() {
low, high := minMax([]int{3, 1, 4, 1, 5})
fmt.Println(low, high)
} def min_max(numbers):
return min(numbers), max(numbers)
low, high = min_max([3, 1, 4, 1, 5])
print(low, high) Python "multiple return" is really returning a single
tuple that the caller unpacks — low, high = min_max(...). It looks like Go's multiple return values but is one object under the hood, which is why you can also capture it whole (result = min_max(...)) and index it. The built-ins min and max save the manual loop entirely.Comprehensions
List comprehensions
package main
import "fmt"
func main() {
numbers := []int{1, 2, 3, 4, 5}
squares := []int{}
for _, number := range numbers {
if number%2 == 0 {
squares = append(squares, number*number)
}
}
fmt.Println(squares)
} numbers = [1, 2, 3, 4, 5]
squares = [number * number for number in numbers if number % 2 == 0]
print(squares) A list comprehension builds a list in a single expression:
[expr for item in iterable if condition]. It replaces the explicit map-and-filter loop Go must write out, reads close to the mathematical "set of squares where…", and is the single most characteristic piece of idiomatic Python. The whole loop above collapses to one line.Dict & set comprehensions
package main
import "fmt"
func main() {
words := []string{"apple", "banana", "cherry"}
lengths := map[string]int{}
for _, word := range words {
lengths[word] = len(word)
}
fmt.Println(lengths["banana"])
} words = ["apple", "banana", "cherry"]
lengths = {word: len(word) for word in words}
print(lengths["banana"])
unique_lengths = {len(word) for word in words} # a set
print(sorted(unique_lengths)) The comprehension form extends to dicts (
{key: value for ...}) and sets ({value for ...}). Each builds the whole collection in one expression where Go needs a loop with explicit insertion. A set is Python's built-in unordered collection of unique values — Go has no native set type and emulates one with a map[T]bool.Generator expressions
package main
import "fmt"
func main() {
numbers := []int{1, 2, 3, 4, 5}
total := 0
for _, number := range numbers {
total += number * number
}
fmt.Println(total)
} numbers = [1, 2, 3, 4, 5]
total = sum(number * number for number in numbers)
print(total) Writing the comprehension with parentheses instead of brackets produces a generator — a lazy iterator that yields one value at a time instead of building a whole list in memory. Passed straight to
sum() (or max, any, all), it computes the result with no intermediate allocation, the Python equivalent of a streaming fold.Classes & OOP
Classes
package main
import "fmt"
type Point struct {
X, Y int
}
func main() {
point := Point{X: 1, Y: 2}
fmt.Println(point.X, point.Y)
} class Point:
def __init__(self, x, y):
self.x = x
self.y = y
point = Point(1, 2)
print(point.x, point.y) Python has real classes, which Go lacks entirely. State lives in instance attributes set on
self inside the __init__ constructor, and self is an explicit first parameter on every method (there is no implicit receiver). Construction is just calling the class — Point(1, 2) — with no new keyword.Inheritance
package main
import "fmt"
// Go has no inheritance; it composes via embedding.
type Animal struct{ Name string }
func (animal Animal) Speak() string { return animal.Name + " makes a sound" }
type Dog struct{ Animal }
func main() {
dog := Dog{Animal{Name: "Rex"}}
fmt.Println(dog.Speak())
} class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound"
class Dog(Animal):
def speak(self):
return super().speak() + " (woof)"
print(Dog("Rex").speak()) Python has classical inheritance —
class Dog(Animal) — including method overriding and super() to call the parent's version, plus multiple inheritance. Go deliberately omits inheritance and composes behaviour by embedding one struct in another. This "is-a" hierarchy is one of the biggest conceptual additions Python brings over Go's flat composition.Dunder (operator) methods
package main
import "fmt"
// Go has no operator overloading; you write named methods.
type Vector struct{ X, Y int }
func (vector Vector) Add(other Vector) Vector {
return Vector{vector.X + other.X, vector.Y + other.Y}
}
func main() {
sum := Vector{1, 2}.Add(Vector{3, 4})
fmt.Println(sum)
} class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
print(Vector(1, 2) + Vector(3, 4)) Python lets a class hook into language operators through "dunder" (double-underscore) methods:
__add__ makes + work, __repr__ controls how the object prints, __eq__ defines ==, __len__ enables len(). Go has no operator overloading at all, so equivalent behaviour is always a named method call like .Add(...).Dataclasses
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
first := Person{"Alice", 30}
second := Person{"Alice", 30}
fmt.Println(first == second) // structs compare field-by-field
} from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
first = Person("Alice", 30)
second = Person("Alice", 30)
print(first == second) # dataclasses get __eq__ for free
print(first) # and a readable __repr__ The
@dataclass decorator auto-generates __init__, __repr__, and __eq__ from the field annotations — the closest Python comes to a Go struct that compares field-by-field and prints cleanly. Without it, a plain class compares by identity (is), so two equal-looking objects are not == unless you define __eq__ yourself.Duck Typing
Duck typing vs interfaces
package main
import "fmt"
type Speaker interface{ Speak() string }
type Cat struct{}
func (cat Cat) Speak() string { return "meow" }
func announce(speaker Speaker) { fmt.Println(speaker.Speak()) }
func main() {
announce(Cat{})
} class Cat:
def speak(self):
return "meow"
def announce(speaker):
print(speaker.speak())
announce(Cat()) # no interface — any object with .speak() works Go has structural interfaces: a type satisfies
Speaker by having a Speak method, checked at compile time. Python drops the declaration entirely — announce just calls .speak(), and any object that happens to have that method works. "If it walks like a duck and quacks like a duck," with errors surfacing at runtime rather than compile time.Runtime type checks
package main
import "fmt"
func describe(value any) string {
switch typed := value.(type) {
case int:
return fmt.Sprintf("int: %d", typed)
case string:
return fmt.Sprintf("string: %s", typed)
default:
return "unknown"
}
}
func main() {
fmt.Println(describe(42))
fmt.Println(describe("hi"))
} def describe(value):
if isinstance(value, int):
return f"int: {value}"
if isinstance(value, str):
return f"string: {value}"
return "unknown"
print(describe(42))
print(describe("hi")) Go inspects a dynamic type with a type switch on an
any value. Python uses isinstance(value, Type), which also respects inheritance (a subclass instance passes its parent's check). Idiomatic Python often prefers to skip the check entirely and simply attempt the operation, catching the exception if the object does not support it ("easier to ask forgiveness than permission").Error Handling
Exceptions vs (value, error)
package main
import (
"errors"
"fmt"
)
func divide(numerator, denominator int) (int, error) {
if denominator == 0 {
return 0, errors.New("division by zero")
}
return numerator / denominator, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(result)
} def divide(numerator, denominator):
if denominator == 0:
raise ValueError("division by zero")
return numerator // denominator
try:
print(divide(10, 0))
except ValueError as error:
print("error:", error) This is the deepest difference for a Go programmer. Go returns errors as values you check after every call; Python raises exceptions that unwind the stack until a
try/except catches them. The happy path stays free of if err != nil, and an unhandled exception propagates automatically with a full traceback rather than being silently ignorable.try / except / finally
package main
import "fmt"
func process() {
defer fmt.Println("cleanup runs last")
fmt.Println("working")
}
func main() {
process()
} def process():
try:
print("working")
finally:
print("cleanup runs last")
process() Python's
finally block plays the role of Go's defer: it runs whether the try body succeeds or raises. You can also pair multiple except clauses for different exception types and an else that runs only on success. Unlike defer, it is attached to a try block rather than scheduled per statement.Custom exceptions
package main
import "fmt"
type NotFoundError struct{ Key string }
func (notFound NotFoundError) Error() string {
return "not found: " + notFound.Key
}
func main() {
var err error = NotFoundError{Key: "user:42"}
fmt.Println(err)
} class NotFoundError(Exception):
pass
try:
raise NotFoundError("user:42")
except NotFoundError as error:
print("not found:", error) A custom Python exception is a class that inherits from
Exception — often just pass for the body. except then matches it by class, and because exceptions form a hierarchy you can catch a whole family at once (catching Exception catches everything below it). Go's custom errors are any value implementing Error() string, matched by type assertion instead.Context managers (with)
package main
import "fmt"
// Go uses defer to guarantee cleanup.
func main() {
// file, _ := os.Open("data.txt")
// defer file.Close()
fmt.Println("file would be closed by defer")
} class Resource:
def __enter__(self):
print("acquire")
return self
def __exit__(self, *exception):
print("release")
with Resource():
print("using") A
with block uses a context manager to guarantee setup and teardown: __enter__ runs on entry and __exit__ always runs on exit, even if an exception is raised. It is Python's structured answer to Go's defer file.Close() — the canonical use is with open(path) as file:, which closes the file automatically when the block ends.Iterators & Generators
Generators (yield)
package main
import "fmt"
// Go 1.23+ has range-over-func iterators; classic Go returns a slice.
func firstSquares(count int) []int {
result := []int{}
for index := 1; index <= count; index++ {
result = append(result, index*index)
}
return result
}
func main() {
fmt.Println(firstSquares(5))
} def first_squares(count):
for number in range(1, count + 1):
yield number * number
for square in first_squares(5):
print(square) A function containing
yield is a generator: calling it returns a lazy iterator that produces values one at a time, suspending and resuming at each yield. It computes on demand with no full list in memory — ideal for large or infinite sequences. Go's closest equivalent is the range-over-func iterator added in 1.23, but generators are far more concise.zip & enumerate
package main
import "fmt"
func main() {
names := []string{"Alice", "Bob"}
ages := []int{30, 25}
for index := range names {
fmt.Printf("%s is %d\n", names[index], ages[index])
}
} names = ["Alice", "Bob"]
ages = [30, 25]
for name, age in zip(names, ages):
print(f"{name} is {age}") zip() walks several iterables in lockstep, yielding tuples of corresponding elements — so you iterate two parallel lists without index bookkeeping. Combined with enumerate() (value plus index) and unpacking, these built-ins replace the manual index loops Go writes. zip stops at the shortest input.The iterator protocol
package main
import "fmt"
func main() {
// Go: iteration is built into range; there is no iterator object.
for index := 0; index < 3; index++ {
fmt.Println(index)
}
} class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
self.current -= 1
return self.current + 1
for value in Countdown(3):
print(value) Any Python object becomes iterable by implementing
__iter__ (return the iterator) and __next__ (return the next value or raise StopIteration). The for loop, comprehensions, and list() all speak this protocol, so a custom class drops straight into them. Go has no such object protocol; iteration is a language built-in tied to range.Concurrency
Threads & the GIL
package main
import (
"fmt"
"sync"
)
func main() {
results := make([]int, 4)
var waitGroup sync.WaitGroup
for index := 0; index < 4; index++ {
waitGroup.Add(1)
go func(position int) {
defer waitGroup.Done()
results[position] = position * position
}(index)
}
waitGroup.Wait()
fmt.Println(results)
} import threading
results = [0] * 4
def worker(position):
results[position] = position * position
threads = [threading.Thread(target=worker, args=(index,)) for index in range(4)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
print(results) Python's
threading mirrors Go's goroutines in shape — start workers, then join them — but a caution for Go programmers: the standard CPython interpreter has a Global Interpreter Lock, so threads do not run pure-Python code in parallel. They help with I/O-bound work, not CPU-bound; for true parallelism Python uses multiprocessing or releases the lock in C extensions.asyncio vs goroutines
package main
import (
"fmt"
"sync"
)
func main() {
var waitGroup sync.WaitGroup
results := make([]string, 2)
for index, label := range []string{"a", "b"} {
waitGroup.Add(1)
go func(position int, name string) {
defer waitGroup.Done()
results[position] = name + " done"
}(index, label)
}
waitGroup.Wait()
fmt.Println(results)
} import asyncio
async def task(label):
await asyncio.sleep(0.01)
return f"{label} done"
async def main():
results = await asyncio.gather(task("a"), task("b"))
print(results)
asyncio.run(main()) Go's goroutines are scheduled by the runtime and look synchronous; Python instead has explicit
async/await with an event loop you launch via asyncio.run. Functions must be declared async to be awaited, and asyncio.gather runs several concurrently. This colours functions "async" in a way Go avoids — every goroutine is just a function with go in front.Channels vs queues
package main
import "fmt"
func main() {
messages := make(chan int, 3)
for index := 0; index < 3; index++ {
messages <- index * index
}
close(messages)
for value := range messages {
fmt.Println(value)
}
} import queue
messages = queue.Queue()
for index in range(3):
messages.put(index * index)
results = []
while not messages.empty():
results.append(messages.get())
print(results) Go's channels are a language primitive for passing values between goroutines. Python's nearest equivalent is
queue.Queue from the standard library, a thread-safe FIFO: producers put and consumers get (which blocks until an item is available). There is no select statement and no language-level channel — communication is a library facility, not syntax.Decorators & Modules
Decorators
package main
import "fmt"
// Go has no decorators; wrap manually with a higher-order function.
func logged(operation func(int) int) func(int) int {
return func(value int) int {
fmt.Println("calling")
return operation(value)
}
}
func main() {
double := logged(func(value int) int { return value * 2 })
fmt.Println(double(5))
} def logged(operation):
def wrapper(value):
print("calling")
return operation(value)
return wrapper
@logged
def double(value):
return value * 2
print(double(5)) A decorator is syntactic sugar for wrapping a function:
@logged above def double means double = logged(double). It is the same higher-order-function idea Go can express manually, but the @ syntax applies it declaratively at definition time. Built-in decorators like @staticmethod, @property, and @dataclass are everywhere in idiomatic Python.Modules & imports
package main
import (
"fmt"
"math"
"strings"
)
func main() {
fmt.Println(math.Sqrt(16))
fmt.Println(strings.ToUpper("go"))
} import math
from string import capwords
print(math.sqrt(16))
print(capwords("hello world")) Python's
import math brings in a module accessed as math.sqrt, while from module import name pulls a single name into scope. Every .py file is a module, and packages are directories — there is no go.mod; third-party libraries come from PyPI via pip. Crucially, Python's vast standard library and package ecosystem are a major draw over Go's leaner standard set.Everything is an object
package main
import "fmt"
func main() {
// Functions are values in Go, but ints and types are not objects.
operation := func(value int) int { return value * 2 }
fmt.Println(operation(21))
} def double(value):
return value * 2
# Functions, classes, modules, and even ints are objects:
operations = {"double": double}
print(operations["double"](21))
print((5).bit_length()) # int is an object with methods
print(double.__name__) # functions carry attributes In Python everything is a first-class object — functions, classes, modules, and even integers all have attributes and methods and can be stored in data structures. You can put functions in a dict and call them by key, ask an
int for its bit_length(), or read a function's __name__. Go has first-class functions but no uniform object model: an int has no methods and a type is not a value.