A comprehensive cheat-sheet comparing Go and Python (3.10+) syntax — fundamentals, OOP, type hints, error handling, async/concurrency, and modern features.
Note on paradigm: Go is statically typed, compiled, and designed for explicit concurrency. Python is dynamically typed (with optional type hints), interpreted, and multi-paradigm (imperative, OOP, functional). Both are known for readability and productivity.
- Naming Convention
- Module & Package
- Import
- Basic Types
- Type Hints (Python)
- Custom Type
- Enum
- Variable
- Function / Method
- If Statement
- Loops
- Match Statement (Python 3.10+)
- Class & Struct
- Interface & Protocol
- Pointer & Reference
- List & Slice
- Map / Dictionary
- Set
- Tuple
- Null Handling
- Error Handling
- Generics
- Closures & Anonymous Functions
- Type Casting & Conversion
- String Operations
- Comprehensions
- Decorators (Python only)
- Context Managers (Python only)
- Generators & Iterators
- Dataclasses (Python only)
- Async & Concurrency
| Go | Python | |
|---|---|---|
| Local variables | camelCase |
snake_case |
| Exported / Public | PascalCase |
No prefix (no keyword) |
| Private / Internal | camelCase (package-private) |
_single_underscore (convention) |
| Name-mangled private | N/A | __double_underscore (class-level) |
| Constants | PascalCase or ALL_CAPS |
ALL_CAPS |
| Types / Classes / Structs | PascalCase |
PascalCase |
| Functions / Methods | camelCase |
snake_case |
| Modules / Packages | lowercase |
lowercase or snake_case |
| Magic methods | N/A | __dunder__ |
In Go, visibility is controlled by casing: uppercase first letter = exported. There are no public/private keywords.
In Python, visibility is a convention only. _name means "internal, don't use from outside". __name in a class triggers name mangling (becomes _ClassName__name). Python has no true access control — it's all convention.
# Python
my_local = 1
MY_CONSTANT = 42
_internal_helper = "private by convention"
class MyClass:
public_attr = 1
_protected = 2 # convention: don't access from outside
__private = 3 # name-mangled to _MyClass__private
def public_method(self): pass
def _protected_method(self): pass
def __private_method(self): passSources:
Go — every .go file declares a package; packages map to directories; go.mod defines the module root:
package main
package myapp
// All .go files in the same directory share the same package namePython — a file is a module; a directory with __init__.py is a package:
# myapp/utils.py — this is the module myapp.utils
# myapp/__init__.py — this makes myapp/ a package
# myapp/sub/__init__.py — sub-package
# Entry point
if __name__ == "__main__":
main()| Go | Python | |
|---|---|---|
| Module unit | Package (directory) | File (.py) |
| Package = directory | Yes | Directory + __init__.py |
| Dependency tool | go mod |
pip + pyproject.toml / requirements.txt |
| Build tool | go build |
python -m build, setuptools, hatch, uv |
| REPL | N/A (third-party) | python / ipython |
Sources:
Go:
import "fmt"
import (
"fmt"
"net/http"
myhttp "net/http" // alias
_ "image/png" // side-effects only
)Python:
# Import a module
import os
import sys
# Import specific names from a module
from os.path import join, exists
from typing import Optional, Union, List
# Alias
import numpy as np
import pandas as pd
from collections import defaultdict as dd
# Import all (avoid — pollutes namespace)
from math import *
# Relative imports (within a package)
from . import sibling_module
from .. import parent_module
from .utils import helper
# Conditional / lazy import (for optional dependencies)
try:
import ujson as json
except ImportError:
import json
# __future__ — enable newer syntax in older Python versions
from __future__ import annotations # PEP 563: postponed evaluation of annotationsKey difference: Go unused imports are a compile error. Python unused imports are a style warning (
flake8/ruff) but not an error.
Sources:
| Category | Go | Python | Notes |
|---|---|---|---|
| Boolean | bool (true/false) |
bool (True/False) |
Python bools are subclass of int |
| Integer | int, int8…int64, uint… |
int |
Python int is arbitrary precision |
| Float | float32, float64 |
float |
64-bit IEEE 754 |
| Complex | complex64, complex128 |
complex |
|
| String | string (immutable UTF-8) |
str (immutable Unicode) |
|
| Bytes | []byte |
bytes, bytearray |
|
| None/Nil | nil (nilable types only) |
None |
|
| Any | any (interface{}) |
object / Any (typing) |
// Go: explicit types, zero values
var i int = 42
var f float64 = 3.14
var s string = "hello"
var b bool = true# Python: dynamic typing, no zero values
i = 42
f = 3.14
s = "hello"
b = True
n = None
# Python has arbitrary precision integers
big = 10 ** 100 # no overflow!
# Complex numbers
c = 3 + 4j
c.real # 3.0
c.imag # 4.0Sources:
Python is dynamically typed, but type hints (PEP 484, 3.5+) enable static analysis via tools like mypy, pyright, and ruff. They have no runtime effect unless explicitly used with typing.get_type_hints(). Go types are enforced by the compiler.
# Variable annotations (PEP 526)
name: str = "Alice"
age: int = 30
ratio: float = 0.5
active: bool = True
# Function annotations
def greet(name: str, times: int = 1) -> str:
return (f"Hello, {name}! " * times).strip()
# Optional — value or None (Python 3.10+ shorthand: str | None)
from typing import Optional
def find_user(id: int) -> Optional[str]: # old style
...
def find_user(id: int) -> str | None: # Python 3.10+ union syntax
...
# Union types
def process(value: int | str | float) -> str:
return str(value)
# Collections
from typing import List, Dict, Tuple, Set
def process_list(items: list[int]) -> list[str]: # 3.9+: built-in generics
return [str(i) for i in items]
# Callable
from typing import Callable
def apply(fn: Callable[[int, int], int], a: int, b: int) -> int:
return fn(a, b)
# TypeVar — generics
from typing import TypeVar, Generic
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
# Literal types
from typing import Literal
def set_direction(d: Literal["left", "right", "up", "down"]) -> None: ...
# TypedDict — typed dictionary
from typing import TypedDict
class UserDict(TypedDict):
name: str
age: int
email: str | None
# Final — constant (no reassignment)
from typing import Final
MAX_SIZE: Final = 1024
# Protocol — structural subtyping (like Go interface)
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
# ParamSpec and Concatenate (3.10+) — for decorator typing
from typing import ParamSpec, Concatenate
P = ParamSpec("P")
# type statement (3.12+) — new syntax for type aliases
type Vector = list[float]
type Matrix[T] = list[list[T]]Sources:
Go:
// Distinct type (not interchangeable with base)
type Age int16
type UserID string
// Struct
type Person struct {
Name string
Age Age
}
// Interface
type Writer interface {
Write([]byte) (int, error)
}Python:
from typing import NewType, TypeAlias
# NewType — creates a distinct type for static checkers only (no runtime cost)
UserId = NewType("UserId", int)
PostId = NewType("PostId", int)
user_id = UserId(42)
# type alias (Python 3.12+ syntax)
type Vector = list[float]
# Old style alias (pre-3.12)
Vector: TypeAlias = list[float]
# TypedDict — typed dictionary shape (not a class)
from typing import TypedDict
class Point(TypedDict):
x: float
y: float
p: Point = {"x": 1.0, "y": 2.0}
# Named tuple — immutable, typed, named fields
from typing import NamedTuple
class Person(NamedTuple):
name: str
age: int
email: str | None = None # default value
alice = Person("Alice", 30)
alice.name # "Alice"
alice[0] # "Alice" (also indexable as tuple)
# Class (see Class & Struct section)
class MyType:
def __init__(self, value: int) -> None:
self.value = valueKey difference: Go's
type Age int16creates a distinct type the compiler enforces. Python'sNewTypeis only checked by static analysis tools (mypy/pyright) — at runtime it's just the underlying type.
Sources:
Go — uses iota inside a const block:
type Status int
const (
Active Status = iota // 0
Inactive // 1
Deleted // 2
)
type Color string
const (
Red Color = "red"
Green Color = "green"
)Python — first-class Enum class:
from enum import Enum, IntEnum, StrEnum, Flag, auto
# Basic enum
class Status(Enum):
ACTIVE = "active"
INACTIVE = "inactive"
DELETED = "deleted"
Status.ACTIVE # <Status.ACTIVE: 'active'>
Status.ACTIVE.value # "active"
Status.ACTIVE.name # "ACTIVE"
Status("active") # Status.ACTIVE — lookup by value
# Integer enum (also an int — comparison works with plain ints)
class Priority(IntEnum):
LOW = 1
MEDIUM = 2
HIGH = 3
Priority.HIGH > Priority.LOW # True
# String enum (Python 3.11+ — also a str)
class Color(StrEnum):
RED = "red"
GREEN = "green"
BLUE = "blue"
Color.RED + " sky" # "red sky"
# auto() — auto-assign values
class Direction(Enum):
NORTH = auto()
EAST = auto()
SOUTH = auto()
WEST = auto()
# Flag — combinable bit flags (like Go's const iota bit flags)
class Permission(Flag):
READ = auto() # 1
WRITE = auto() # 2
EXECUTE = auto() # 4
ALL = READ | WRITE | EXECUTE
perms = Permission.READ | Permission.WRITE
Permission.READ in perms # True
# Iterate
for status in Status:
print(status, status.value)Key difference: Python enums are full classes — they can have methods. Go
iotaconstants are bare integer/string values with no extra behavior.
Sources:
Go:
var name string = "Alice"
age := 30
const Pi = 3.14159
// Zero values always provided
var i int // 0
var s string // ""Python:
# Assignment — no declaration keyword (unlike Go's var)
name = "Alice"
age = 30
# Type annotation only (no assignment)
count: int # declares type, no value — accessing raises NameError
# Constants — convention only (Python has no true const)
MAX_SIZE = 1024
PI = 3.14159
# Multiple assignment
x, y = 1, 2
a = b = c = 0 # chain assignment
# Augmented assignment
x += 1
x -= 1
x *= 2
x //= 3 # floor division assignment
x **= 2 # power assignment
# Walrus operator := (Python 3.8+) — assign and use in expression
if n := len(data):
print(f"data has {n} elements")
while chunk := file.read(8192):
process(chunk)
# Global and nonlocal
x = 10
def outer():
y = 20
def inner():
nonlocal y # modify outer's y
global x # modify module-level x
y = 30
x = 99Key difference: Python has no zero values — an unassigned variable name raises
NameError. Go always initializes to zero.
Sources:
Go:
func greet(name string) string {
return "Hello, " + name
}
func divide(a, b float64) (float64, error) {
if b == 0 { return 0, errors.New("division by zero") }
return a / b, nil
}
func sum(nums ...int) int {
total := 0
for _, n := range nums { total += n }
return total
}
// Method
type Circle struct{ Radius float64 }
func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius }Python:
# Basic function
def greet(name: str) -> str:
return f"Hello, {name}"
# Default parameters
def create_user(name: str, role: str = "user", active: bool = True) -> dict:
return {"name": name, "role": role, "active": active}
# *args (variadic positional) and **kwargs (variadic keyword)
def log(*args: str, level: str = "INFO", **kwargs: str) -> None:
print(f"[{level}]", *args, kwargs)
log("message", "extra", level="DEBUG", tag="auth")
# Keyword-only arguments (after *)
def connect(host: str, *, port: int = 80, timeout: float = 30.0) -> None:
...
connect("example.com", port=443) # ok
# connect("example.com", 443) # TypeError — port is keyword-only
# Positional-only arguments (before /) — Python 3.8+
def make_point(x: float, y: float, /) -> tuple[float, float]:
return x, y
make_point(1.0, 2.0) # ok
# make_point(x=1.0, y=2.0) # TypeError — x, y are positional-only
# Multiple return values (returns a tuple)
def min_max(nums: list[int]) -> tuple[int, int]:
return min(nums), max(nums)
lo, hi = min_max([3, 1, 4, 1, 5])
# Functions are first-class objects
def apply(fn, items):
return [fn(x) for x in items]
apply(str.upper, ["a", "b"]) # ["A", "B"]
# Method in a class
class Circle:
def __init__(self, radius: float) -> None:
self.radius = radius
def area(self) -> float:
import math
return math.pi * self.radius ** 2
@classmethod
def unit(cls) -> "Circle":
return cls(1.0)
@staticmethod
def validate(r: float) -> bool:
return r > 0Key differences:
- Python has no named return values (use a tuple). Go has multiple return values and named returns.
- Python supports keyword arguments, keyword-only, and positional-only params. Go does not have keyword arguments (except via struct options pattern).
- Python functions are objects that can have attributes. Go functions are values but not objects.
Sources:
Go — braces required, no parentheses around condition:
if x > 0 {
fmt.Println("positive")
} else if x == 0 {
fmt.Println("zero")
} else {
fmt.Println("negative")
}
// Init statement
if x := compute(); x < 0 {
fmt.Println("negative:", x)
}Python — uses indentation (no braces); elif keyword:
if x > 0:
print("positive")
elif x == 0:
print("zero")
else:
print("negative")
# Ternary / conditional expression
label = "positive" if x > 0 else ("zero" if x == 0 else "negative")
# Walrus in if
if (n := len(data)) > 10:
print(f"too many items: {n}")
# Truthiness — Python evaluates many types as boolean
if my_list: # True if list is non-empty
...
if my_dict: # True if dict is non-empty
...
if name: # True if string is non-empty
...Sources:
Go — one loop keyword: for:
for i := 0; i < 10; i++ { fmt.Println(i) }
for sum < 1000 { sum += sum }
for { break }
for i, v := range []int{1, 2, 3} { fmt.Println(i, v) }Python — for and while:
# for...in — iterate over any iterable
for i in range(10):
print(i)
for v in [1, 2, 3]:
print(v)
# enumerate — index + value (like Go's range)
for i, v in enumerate(["a", "b", "c"]):
print(i, v)
# zip — iterate multiple iterables together
for name, age in zip(names, ages):
print(name, age)
# while
i = 0
while i < 10:
i += 1
# for...else — else block runs if loop completes without break
for item in collection:
if matches(item):
break
else:
print("no match found")
# range variants
range(10) # 0–9
range(1, 11) # 1–10
range(0, 10, 2) # 0, 2, 4, 6, 8 (step=2)
range(10, 0, -1) # 10, 9, 8, ..., 1 (reverse)
# reversed and sorted
for v in reversed([1, 2, 3]):
print(v)
for v in sorted([3, 1, 2]):
print(v)
# continue and break work as in Go
for i in range(10):
if i % 2 == 0:
continue
if i > 7:
break
print(i)Sources:
Python's match is structural pattern matching — much more powerful than Go's switch. The closest Go feature is a type switch combined with struct field access.
Go:
switch v := x.(type) {
case int:
fmt.Println("int:", v)
case string:
fmt.Println("string:", v)
}Python:
# Basic match (like switch)
match command:
case "quit":
quit()
case "help":
show_help()
case _: # wildcard (default)
print(f"Unknown: {command}")
# Match with value capture
match point:
case (0, 0):
print("origin")
case (0, y):
print(f"y-axis at {y}")
case (x, 0):
print(f"x-axis at {x}")
case (x, y):
print(f"point at ({x}, {y})")
# Match on type (like Go type switch)
match value:
case int(n) if n > 0:
print(f"positive int: {n}")
case str(s):
print(f"string: {s}")
case list() as lst if len(lst) > 0:
print(f"non-empty list: {lst}")
# Struct/class pattern matching
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
match shape:
case Point(x=0, y=0):
print("origin")
case Point(x=x, y=y):
print(f"point at {x}, {y}")
# OR patterns
match status:
case 400 | 401 | 403:
print("client error")
case 500 | 502 | 503:
print("server error")
# Sequence patterns
match command.split():
case ["quit"]:
quit()
case ["go", direction]:
go(direction)
case ["go", direction, distance]:
go(direction, int(distance))
case ["drop", *objects]:
drop_all(objects)
# Mapping patterns (dict)
match config:
case {"action": "push", "target": target}:
push(target)
case {"action": "pull", "source": src}:
pull(src)Sources:
Go — no classes; uses structs + methods + interfaces:
type Person struct {
Name string
Age int
}
func NewPerson(name string, age int) *Person {
return &Person{Name: name, Age: age}
}
func (p Person) Greet() string {
return "Hello, I'm " + p.Name
}
func (p *Person) Birthday() {
p.Age++
}
// Embedding (composition)
type Employee struct {
Person
Company string
}Python:
class Person:
# Class variable (shared across all instances)
species: str = "Homo sapiens"
def __init__(self, name: str, age: int) -> None:
# Instance variables
self.name = name
self.age = age
# Instance method
def greet(self) -> str:
return f"Hello, I'm {self.name}"
# String representation
def __repr__(self) -> str:
return f"Person(name={self.name!r}, age={self.age})"
# Equality
def __eq__(self, other: object) -> bool:
if not isinstance(other, Person):
return NotImplemented
return self.name == other.name and self.age == other.age
# Class method (alternative constructor)
@classmethod
def from_dict(cls, data: dict) -> "Person":
return cls(data["name"], data["age"])
# Static method (no access to class or instance)
@staticmethod
def validate_age(age: int) -> bool:
return 0 <= age <= 150
# Inheritance
class Employee(Person):
def __init__(self, name: str, age: int, company: str) -> None:
super().__init__(name, age) # call parent __init__
self.company = company
def greet(self) -> str: # override
return f"{super().greet()} from {self.company}"
# Multiple inheritance (Python supports it; Go does not)
class Flyable:
def fly(self) -> str: return "I can fly"
class FlyingEmployee(Employee, Flyable):
pass
# Property — computed attribute (getter/setter)
class Temperature:
def __init__(self, celsius: float) -> None:
self._celsius = celsius
@property
def celsius(self) -> float:
return self._celsius
@celsius.setter
def celsius(self, value: float) -> None:
if value < -273.15:
raise ValueError("Below absolute zero")
self._celsius = value
@property
def fahrenheit(self) -> float:
return self._celsius * 9 / 5 + 32
t = Temperature(100)
t.fahrenheit # 212.0
t.celsius = 0Key differences:
- Go has no inheritance — use embedding. Python supports full class-based inheritance including multiple inheritance.
- Go constructors are regular functions. Python uses
__init__(initializer, not allocator).- Python classes have magic methods (
__repr__,__eq__,__len__, etc.) for operator overloading. Go uses interfaces for this.
Sources:
Go — interfaces are satisfied implicitly (structural typing):
type Stringer interface {
String() string
}
type Area interface {
Area() float64
}
// Circle satisfies Stringer implicitly
type Circle struct{ Radius float64 }
func (c Circle) String() string { return fmt.Sprintf("Circle(r=%.2f)", c.Radius) }
func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius }Python — uses abstract base classes (abc.ABC) or typing.Protocol (structural):
from abc import ABC, abstractmethod
from typing import Protocol, runtime_checkable
# Abstract base class (nominal subtyping — must explicitly inherit)
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
@abstractmethod
def perimeter(self) -> float: ...
# Concrete method in abstract class
def describe(self) -> str:
return f"Area: {self.area():.2f}, Perimeter: {self.perimeter():.2f}"
class Circle(Shape):
def __init__(self, radius: float) -> None:
self.radius = radius
def area(self) -> float:
import math
return math.pi * self.radius ** 2
def perimeter(self) -> float:
import math
return 2 * math.pi * self.radius
# Protocol — structural subtyping (like Go interface — no inheritance needed)
@runtime_checkable # enables isinstance() checks
class Drawable(Protocol):
def draw(self) -> None: ...
class Canvas:
def draw(self) -> None:
print("drawing on canvas")
# Canvas satisfies Drawable without inheriting it (structural typing)
def render(d: Drawable) -> None:
d.draw()
render(Canvas()) # works!
# isinstance check (requires @runtime_checkable)
isinstance(Canvas(), Drawable) # True
# Built-in protocols via collections.abc
from collections.abc import Sequence, Mapping, Iterable, Iterator, Callable
def process(items: Iterable[int]) -> list[int]:
return [x * 2 for x in items]Key differences:
- Go interfaces are always structural (implicit). Python ABC requires explicit inheritance;
Protocolis structural (like Go).- Python
Protocol(PEP 544) is the direct equivalent of Go interfaces — both use duck typing without requiring explicitimplements.
Sources:
Go — explicit pointers:
x := 42
p := &x // pointer to x
*p = 100 // modify through pointer; x is now 100
func increment(n *int) { *n++ }
increment(&x)Python — no pointers; mutable objects passed by reference, immutable by value effect:
# Immutable types (int, str, float, tuple, frozenset): behave like pass-by-value
x = 42
def change(n):
n = 100 # local rebinding, x unchanged
change(x)
print(x) # 42
# Mutable types (list, dict, set, objects): shared reference
lst = [1, 2, 3]
def append_item(l):
l.append(4) # mutates the original list
append_item(lst)
print(lst) # [1, 2, 3, 4]
# To avoid mutation, pass a copy
append_item(lst.copy())
# Shallow vs deep copy
import copy
shallow = copy.copy(nested_obj) # copies top-level, shares nested
deep = copy.deepcopy(nested_obj) # fully independent copy
# "Pass by assignment" — Python passes the object reference
# Rebinding the parameter doesn't affect the caller; mutating doesSources:
Go:
s := []int{1, 2, 3}
s = append(s, 4)
sub := s[1:3]
len(s); cap(s)Python:
# List (dynamic, heterogeneous, reference type)
lst = [1, 2, 3]
lst.append(4) # add to end
lst.insert(0, 0) # insert at index
lst.extend([5, 6]) # extend with iterable
lst.pop() # remove and return last
lst.pop(0) # remove and return at index
lst.remove(3) # remove first occurrence of value
lst.index(2) # find index of value
del lst[0] # delete by index
del lst[1:3] # delete slice
len(lst)
# Slicing (returns new list — doesn't share memory with original)
lst[1:4] # elements 1, 2, 3
lst[:3] # first 3
lst[2:] # from index 2 to end
lst[-1] # last element
lst[-3:] # last 3 elements
lst[::2] # every other element (step=2)
lst[::-1] # reversed
# Spread / unpack
merged = [*lst, *other]
# Sort (in-place)
lst.sort() # ascending
lst.sort(reverse=True) # descending
lst.sort(key=lambda x: x.name) # by attribute
# Sorted (returns new list)
sorted_lst = sorted(lst)
sorted_lst = sorted(lst, key=len, reverse=True)
# Check membership
3 in lst # True
3 not in lst # False
# List operations
lst.count(2) # count occurrences of 2
lst.reverse() # reverse in place
lst.clear() # empty the list
lst.copy() # shallow copyKey difference: Go slices share underlying array memory with the original. Python list slices create a new independent list.
Sources:
Go:
m := map[string]int{"a": 1, "b": 2}
m["key"] = 42
delete(m, "key")
value, ok := m["key"]Python:
# Create
d = {"name": "Alice", "age": 30}
d = dict(name="Alice", age=30) # keyword form
d = dict([("name", "Alice"), ("age", 30)]) # from iterable of pairs
# Access
d["name"] # "Alice" — KeyError if missing
d.get("missing") # None (no KeyError)
d.get("missing", "default") # "default"
# Update
d["name"] = "Bob"
d.update({"email": "bob@example.com", "age": 31})
d |= {"tag": "user"} # merge in-place (Python 3.9+)
# Delete
del d["age"]
d.pop("age") # remove and return; KeyError if missing
d.pop("age", None) # remove and return None if missing
# Existence check
"name" in d # True
"missing" in d # False
# Iteration
for key in d: # iterate keys (insertion-ordered since 3.7)
print(key, d[key])
for key, value in d.items():
print(key, value)
for key in d.keys(): ...
for value in d.values(): ...
# Merge (Python 3.9+)
merged = d1 | d2 # d2 wins on conflicts
# Default dict — auto-creates missing keys
from collections import defaultdict
counts = defaultdict(int)
for word in words:
counts[word] += 1
# Counter — specialized dict for counting
from collections import Counter
counts = Counter(["a", "b", "a", "c", "a"])
counts.most_common(2) # [("a", 3), ("b", 1)]
# OrderedDict — explicit ordering (redundant since 3.7, kept for .move_to_end)
from collections import OrderedDict
od = OrderedDict(a=1, b=2)
od.move_to_end("a")
# Dict comprehension
squared = {n: n**2 for n in range(1, 6)}
# {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}Key differences:
- Python dicts preserve insertion order since Python 3.7. Go maps do not.
- Python's
d["key"]raisesKeyErroron missing; Go returns zero value. Used.get("key")for safe access.
Sources:
Go has no built-in set type (use map[T]struct{}). Python has first-class set and frozenset:
// Go: set via map
set := map[string]struct{}{}
set["a"] = struct{}{}
_, ok := set["a"] // membership
delete(set, "a")# Python: first-class set
s = {1, 2, 3}
s = set([1, 2, 3])
# Operations
s.add(4)
s.remove(3) # KeyError if missing
s.discard(3) # no error if missing
2 in s # membership: True
# Set operations
a = {1, 2, 3}
b = {2, 3, 4}
a | b # union: {1, 2, 3, 4}
a & b # intersection: {2, 3}
a - b # difference: {1}
a ^ b # symmetric difference: {1, 4}
a.issubset(b) # False
a.issuperset(b) # False
a.isdisjoint(b) # False — they share elements
# Frozenset — immutable, hashable (can be used as dict key or set element)
fs = frozenset([1, 2, 3])Sources:
- Python: Python Docs — set · Python Docs — frozenset
Go has no built-in tuple type. Python tuples are immutable sequences:
// Go: use multiple return values or a struct
func minMax() (int, int) { return 0, 100 }
lo, hi := minMax()# Python: tuple — immutable, ordered, heterogeneous
t = (1, "hello", 3.14)
t = 1, "hello", 3.14 # parentheses optional (tuple packing)
# Access
t[0] # 1
t[-1] # 3.14
# Unpack
x, y, z = t
# Swap (uses tuple packing/unpacking)
a, b = b, a
# Single-element tuple (trailing comma required)
single = (42,)
type(single) # <class 'tuple'>
not_tuple = (42)
type(not_tuple) # <class 'int'>
# Named tuple — adds field names (see Custom Type section)
from typing import NamedTuple
class Point(NamedTuple):
x: float
y: float
p = Point(1.0, 2.0)
p.x # 1.0
p[0] # 1.0 — also indexableSources:
- Python: Python Docs — tuple · Python Docs — NamedTuple
Go — only nilable types can be nil; zero values always provided:
var p *int // nil
var s []int // nil (but len==0)
if p != nil {
fmt.Println(*p)
}Python — None is a singleton object usable as any type's null value:
value = None
# Check for None
if value is None: # recommended (identity check)
print("no value")
if value is not None:
print(value)
# Pitfall: avoid == None
if value == None: # works but not idiomatic; is None is better
# Optional type hint
from typing import Optional
def find(id: int) -> Optional[str]: # str | None
...
# Common patterns
result = maybe_none_value or "default" # default if falsy (None, 0, "", [])
result = maybe_none_value if maybe_none_value is not None else "default" # safe
# Walrus + None
if (user := get_user(id)) is not None:
process(user)Sources:
Go — errors are values; no exceptions:
f, err := os.Open("file.txt")
if err != nil {
return fmt.Errorf("open: %w", err)
}
type NotFoundError struct{ ID int }
func (e *NotFoundError) Error() string {
return fmt.Sprintf("ID %d not found", e.ID)
}Python — uses exceptions (try/except/else/finally):
# Basic try/except/else/finally
try:
result = int("not a number")
except ValueError as e:
print(f"ValueError: {e}")
except (TypeError, KeyError):
print("type or key error")
else:
# Runs if no exception was raised
print(f"result: {result}")
finally:
# Always runs
cleanup()
# Raise an exception
raise ValueError("invalid input")
raise RuntimeError("something went wrong") from original_error # chained
# Custom exception
class NotFoundError(Exception):
def __init__(self, resource: str, id: int) -> None:
self.resource = resource
self.id = id
super().__init__(f"{resource} with id={id} not found")
try:
raise NotFoundError("User", 42)
except NotFoundError as e:
print(e.resource, e.id)
# Exception groups (Python 3.11+)
try:
raise ExceptionGroup("multiple errors", [
ValueError("bad value"),
TypeError("bad type"),
])
except* ValueError as eg:
print("handled ValueErrors:", eg.exceptions)
except* TypeError as eg:
print("handled TypeErrors:", eg.exceptions)
# Context manager for cleanup (see Context Managers section)
with open("file.txt") as f:
content = f.read()
# File is closed automatically
# Suppress specific exceptions
from contextlib import suppress
with suppress(FileNotFoundError):
os.remove("maybe_missing.txt")
# Result pattern (optional, not idiomatic Python)
from typing import Union
def safe_divide(a: float, b: float) -> Union[float, Exception]:
if b == 0:
return ValueError("division by zero")
return a / bKey differences:
- Go uses multiple return values for errors; Python uses exceptions.
- Python exceptions propagate up the call stack automatically. Go errors must be explicitly checked and returned.
- Python
finallyis equivalent to Go'sdeferfor cleanup.
Sources:
Go — type parameters since Go 1.18:
func Map[T, U any](s []T, f func(T) U) []U { ... }
type Stack[T any] struct{ items []T }
func (s *Stack[T]) Push(item T) { s.items = append(s.items, item) }Python — generics via TypeVar (pre-3.12) or type statement (3.12+):
from typing import TypeVar, Generic
T = TypeVar("T")
U = TypeVar("U")
# Generic function (type checker validates; runtime does not)
def first(items: list[T]) -> T:
return items[0]
def map_list(items: list[T], fn: Callable[[T], U]) -> list[U]:
return [fn(x) for x in items]
# Generic class (pre-3.12)
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
def peek(self) -> T:
return self._items[-1]
s: Stack[int] = Stack()
s.push(1)
s.push(2)
s.pop() # 2
# Python 3.12+ — built-in generic syntax (no TypeVar import needed)
def first[T](items: list[T]) -> T:
return items[0]
class Stack[T]:
def push(self, item: T) -> None: ...
def pop(self) -> T: ...
# Bound TypeVar — constrain to specific types
from typing import TypeVar
Numeric = TypeVar("Numeric", int, float, complex)
def add(a: Numeric, b: Numeric) -> Numeric:
return a + b
# Constrained with Protocol
from typing import Protocol, TypeVar
class Comparable(Protocol):
def __lt__(self, other: "Comparable") -> bool: ...
C = TypeVar("C", bound=Comparable)
def min_val(a: C, b: C) -> C:
return a if a < b else bKey difference: Go generics are enforced at compile time. Python generics are only checked by static analysis tools (mypy, pyright) — at runtime,
list[int]is justlistandStack[int]is justStack.
Sources:
Go:
double := func(x int) int { return x * 2 }
func makeCounter() func() int {
count := 0
return func() int { count++; return count }
}Python:
# Lambda — single-expression anonymous function
double = lambda x: x * 2
add = lambda x, y: x + y
# Lambda in sort key
students.sort(key=lambda s: s.grade)
# Lambda with map/filter (list comprehensions are preferred)
doubled = list(map(lambda x: x * 2, [1, 2, 3]))
evens = list(filter(lambda x: x % 2 == 0, range(10)))
# Closure — captures variables by reference
def make_counter():
count = 0
def counter():
nonlocal count # required to modify outer variable
count += 1
return count
return counter
c = make_counter()
c() # 1
c() # 2
# Closure with default argument (capture by value idiom)
funcs = []
for i in range(3):
funcs.append(lambda x, i=i: x + i) # i=i captures current i by value
# Without i=i, all lambdas would capture the final value of i (2)
# Partial application via functools.partial
from functools import partial
def power(base, exp):
return base ** exp
square = partial(power, exp=2)
cube = partial(power, exp=3)
square(5) # 25
cube(3) # 27Key differences:
- Go closures capture by reference automatically. Python closures capture by reference but require
nonlocalto rebind outer variables.- Python lambdas are limited to a single expression. Go function literals support full function bodies.
functools.partialin Python is the equivalent of Go's closure-based partial application pattern.
Sources:
Go:
var i int = 42
f := float64(i)
s := string(65) // "A"
b := []byte("hello")Python:
# Numeric conversion
int("42") # 42
int(3.9) # 3 (truncates, not rounds)
float(42) # 42.0
float("3.14") # 3.14
complex(1, 2) # (1+2j)
# String conversion
str(42) # "42"
str(3.14) # "3.14"
str(True) # "True"
repr(obj) # developer-friendly string (like Go's %v)
# Boolean
bool(0) # False
bool("") # False
bool([]) # False
bool(None) # False
bool(1) # True
bool("a") # True
bool([0]) # True (non-empty list)
# bytes/str
"hello".encode("utf-8") # b"hello"
b"hello".decode("utf-8") # "hello"
# int <-> bytes
(255).to_bytes(2, byteorder="big") # b'\x00\xff'
int.from_bytes(b'\x00\xff', "big") # 255
# Type checking (not casting — Python is duck-typed)
isinstance(value, int) # True if value is int or subclass
isinstance(value, (int, float)) # True if either
type(value) is int # True only for exact type (not subclass)
# No equivalent to Go's transmute — use struct / ctypes for bit reinterpretation
import struct
bits = struct.pack("f", 3.14) # pack float to bytes
val = struct.unpack("f", bits)[0] # unpack bytes to floatSources:
Go:
s := "Hello, 世界"
len(s) // bytes
utf8.RuneCountInString(s) // runes
strings.Contains(s, "世界")
strings.ToUpper(s)
strings.Split("a,b,c", ",")
strings.Join([]string{"a","b"}, ",")
fmt.Sprintf("x=%d", 42)Python:
s = "Hello, 世界"
len(s) # 9 — character count (Unicode)
s.encode("utf-8") # b'Hello, \xe4\xb8\x96\xe7\x95\x8c' — bytes
# Common methods
s.upper()
s.lower()
s.strip()
s.lstrip()
s.rstrip()
s.replace("Hello", "Hi")
s.split(", ") # ["Hello", "世界"]
", ".join(["Hello", "世界"]) # "Hello, 世界"
s.startswith("Hello") # True
s.endswith("界") # True
"世界" in s # True (substring check)
s.find("世界") # 7 (-1 if not found)
s.index("世界") # 7 (ValueError if not found)
s.count("l") # 2
s.center(20, "*") # "***** Hello, 世界 ****"
s.ljust(20, "-")
s.rjust(20, "-")
s.zfill(20)
s.isdigit()
s.isalpha()
s.isalnum()
s.isspace()
# f-strings (format strings) — Python 3.6+
name, age = "Alice", 30
f"Name: {name}, Age: {age}"
f"Pi: {3.14159:.2f}" # "Pi: 3.14"
f"{name!r}" # repr of name: "'Alice'"
f"{name!s}" # str(name)
f"{value:>10}" # right-align in 10 chars
f"{num:,}" # thousands separator
f"{num:08b}" # 8-digit binary with zero-fill
# f-string debugging (Python 3.8+)
x = 42
f"{x=}" # "x=42"
# Multiline strings
text = """
line 1
line 2
line 3
"""
# Raw strings (backslashes not processed)
path = r"C:\Users\Alice\Documents"
pattern = r"\d+\.\d+"
# String formatting alternatives
"Hello, %s! You are %d years old." % (name, age) # old-style (avoid)
"Hello, {}! You are {} years old.".format(name, age) # str.format
"Hello, {name}! You are {age} years old.".format(name=name, age=age)
# textwrap for long strings
import textwrap
wrapped = textwrap.fill(long_text, width=80)
dedented = textwrap.dedent(indented_text)Key differences:
- Python
len(s)counts Unicode characters. Golen(s)counts bytes; useutf8.RuneCountInString(s)for characters.- Python f-strings are native, expressive, and evaluated at runtime. Go uses
fmt.Sprintfwith format verbs.- Python
strmethods return new strings (strings are immutable). Gostringspackage also returns new strings.
Sources:
Go — no built-in comprehensions; use loops:
doubled := make([]int, len(nums))
for i, n := range nums { doubled[i] = n * 2 }Python — list, dict, set, and generator comprehensions:
# List comprehension: [expression for item in iterable if condition]
squares = [x**2 for x in range(10)]
evens = [x for x in range(20) if x % 2 == 0]
flat = [x for row in matrix for x in row] # nested (flatten)
# Dict comprehension
squared = {n: n**2 for n in range(1, 6)}
inverted = {v: k for k, v in d.items()}
# Set comprehension
unique_squares = {x**2 for x in [-2, -1, 0, 1, 2]}
# {0, 1, 4}
# Generator expression (lazy — memory efficient)
total = sum(x**2 for x in range(1_000_000)) # no intermediate list
first_even = next(x for x in data if x % 2 == 0)
# Walrus in comprehension (Python 3.8+)
results = [y for x in data if (y := expensive(x)) is not None]
# Conditional expression in comprehension
labels = ["even" if x % 2 == 0 else "odd" for x in range(10)]Sources:
Decorators are functions that wrap another function or class to modify its behavior. They use the @ syntax. Go has no equivalent — the closest patterns are middleware functions and code generation.
import functools
from typing import Callable, TypeVar, ParamSpec
P = ParamSpec("P")
R = TypeVar("R")
# Basic decorator — wraps a function
def log(fn: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(fn) # preserves function metadata
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"Calling {fn.__name__}")
result = fn(*args, **kwargs)
print(f"{fn.__name__} returned {result!r}")
return result
return wrapper
@log # equivalent to: greet = log(greet)
def greet(name: str) -> str:
return f"Hello, {name}"
greet("Alice")
# Calling greet
# greet returned 'Hello, Alice'
# Decorator factory (decorator with arguments)
def retry(times: int = 3, exceptions: tuple = (Exception,)):
def decorator(fn: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(fn)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
for attempt in range(times):
try:
return fn(*args, **kwargs)
except exceptions as e:
if attempt == times - 1:
raise
print(f"Attempt {attempt + 1} failed: {e}")
return wrapper
return decorator
@retry(times=3, exceptions=(ConnectionError,))
def fetch_data(url: str) -> str:
...
# Class decorator
def singleton(cls):
instances = {}
@functools.wraps(cls)
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class Config:
def __init__(self): self.value = "default"
# Method decorators (built-in)
class MyClass:
class_var = 0
@classmethod # receives class as first arg (cls)
def from_data(cls, data: dict) -> "MyClass":
return cls()
@staticmethod # no self or cls
def utility(x: int) -> int:
return x * 2
@property # computed attribute
def computed(self) -> int:
return self.class_var * 2
# Common standard library decorators
from functools import cached_property, lru_cache
import dataclasses
class Circle:
def __init__(self, radius: float):
self.radius = radius
@cached_property # computed once, then cached
def area(self) -> float:
import math
return math.pi * self.radius ** 2
@lru_cache(maxsize=128) # memoize function calls
def fibonacci(n: int) -> int:
if n < 2: return n
return fibonacci(n - 1) + fibonacci(n - 2)
@dataclasses.dataclass # auto-generate __init__, __repr__, __eq__
class Point:
x: float
y: float
z: float = 0.0Go equivalent: Middleware functions (wrap
http.Handler), higher-order functions, or code generation viago generate.
Sources:
Context managers implement the __enter__/__exit__ protocol and are used with with statements. They are the Python equivalent of Go's defer — ensuring cleanup runs even if an exception occurs.
Go equivalent:
f, err := os.Open("file.txt")
if err != nil { return err }
defer f.Close()
mu.Lock()
defer mu.Unlock()Python:
# File — built-in context manager
with open("file.txt", "r") as f:
content = f.read()
# f is closed automatically (even if exception occurred)
# Multiple contexts in one with
with open("input.txt") as fin, open("output.txt", "w") as fout:
fout.write(fin.read())
# contextlib — decorator/generator approach
from contextlib import contextmanager
@contextmanager
def timer(label: str):
import time
start = time.time()
try:
yield # code inside `with` runs here
finally:
elapsed = time.time() - start
print(f"{label}: {elapsed:.3f}s")
with timer("my_operation"):
expensive_computation()
# contextmanager with value
@contextmanager
def managed_connection(host: str):
conn = connect(host)
try:
yield conn # as conn in `with ... as conn`
except Exception:
conn.rollback()
raise
finally:
conn.close()
with managed_connection("db.example.com") as conn:
conn.execute("SELECT 1")
# Custom context manager class
class Lock:
def __enter__(self):
print("acquiring lock")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("releasing lock")
return False # re-raise any exception
# contextlib.suppress — suppress specific exceptions
from contextlib import suppress
with suppress(FileNotFoundError, PermissionError):
os.remove("maybe_missing.txt")
# contextlib.ExitStack — dynamic number of context managers
from contextlib import ExitStack
with ExitStack() as stack:
files = [stack.enter_context(open(f)) for f in file_list]
process_all(files)Sources:
Go has no generator syntax; iterators are implemented via channels or custom types. Python has first-class generator support:
# Generator function — uses yield
def countdown(n: int):
while n > 0:
yield n # suspends here, resumes on next()
n -= 1
for i in countdown(5):
print(i) # 5 4 3 2 1
# next() — manually advance
gen = countdown(3)
next(gen) # 3
next(gen) # 2
next(gen) # 1
next(gen) # StopIteration
# yield from — delegate to sub-generator
def chain(*iterables):
for it in iterables:
yield from it
list(chain([1,2], [3,4], [5])) # [1, 2, 3, 4, 5]
# Generator with send() — coroutine-like
def accumulator():
total = 0
while True:
value = yield total # receives sent value
if value is None:
break
total += value
acc = accumulator()
next(acc) # prime the generator (advance to first yield)
acc.send(10) # 10
acc.send(5) # 15
acc.send(3) # 18
# Infinite generator
def naturals(start: int = 1):
while True:
yield start
start += 1
import itertools
first_ten = list(itertools.islice(naturals(), 10))
# itertools — composable iterator tools (Go has no equivalent module)
import itertools
itertools.count(1) # infinite counter: 1, 2, 3, ...
itertools.cycle("ABCD") # infinite cycle: A, B, C, D, A, B, ...
itertools.repeat(10, 3) # 10, 10, 10
itertools.chain([1,2], [3,4]) # flatten
itertools.islice(gen, 5) # first 5
itertools.takewhile(pred, it) # take while predicate holds
itertools.dropwhile(pred, it) # drop while predicate holds
itertools.groupby(sorted_data, key) # group consecutive elements
itertools.product([1,2], [3,4]) # cartesian product
itertools.combinations("ABCD", 2) # C(4,2) = 6 combinations
itertools.permutations("ABC", 2) # P(3,2) = 6 permutationsSources:
@dataclass auto-generates __init__, __repr__, __eq__, and optionally __hash__, __lt__, etc. It's Python's equivalent of Go structs — but more feature-rich. Go has no equivalent decorator.
from dataclasses import dataclass, field, asdict, astuple, replace
from typing import ClassVar
@dataclass
class Point:
x: float
y: float
z: float = 0.0 # default value
def distance_from_origin(self) -> float:
return (self.x**2 + self.y**2 + self.z**2) ** 0.5
p = Point(1.0, 2.0)
p # Point(x=1.0, y=2.0, z=0.0) ← __repr__
p == Point(1.0, 2.0) # True ← __eq__
# Immutable dataclass
@dataclass(frozen=True)
class FrozenPoint:
x: float
y: float
# p.x = 5 would raise FrozenInstanceError
# Ordered comparisons
@dataclass(order=True)
class Student:
gpa: float
name: str
students = [Student(3.5, "Alice"), Student(3.9, "Bob")]
sorted(students) # sorted by gpa first, then name
# Fields with defaults and post-init
@dataclass
class Config:
host: str
port: int = 8080
tags: list[str] = field(default_factory=list) # mutable default: use field()
_hash: int = field(init=False, repr=False) # computed, not in __init__
MAX_PORT: ClassVar[int] = 65535 # class variable, not field
def __post_init__(self) -> None:
# Runs after __init__ — for validation or computed fields
if self.port > self.MAX_PORT:
raise ValueError(f"port must be <= {self.MAX_PORT}")
self._hash = hash((self.host, self.port))
# Utilities
asdict(p) # {"x": 1.0, "y": 2.0, "z": 0.0}
astuple(p) # (1.0, 2.0, 0.0)
replace(p, z=5.0) # Point(x=1.0, y=2.0, z=5.0) — like Go's struct literal updateSources:
// Goroutines — lightweight green threads
go someFunction()
ch := make(chan int, 1)
go func() { ch <- 42 }()
v := <-ch
// select: multiplex on channels
select {
case v := <-ch1:
handle(v)
case <-time.After(time.Second):
timeout()
}Python has two concurrency models: asyncio (cooperative, single-threaded) for I/O-bound tasks, and threading/multiprocessing for CPU-bound parallelism:
import asyncio
# async def — defines a coroutine (suspendable function)
async def fetch_user(id: int) -> dict:
await asyncio.sleep(0.1) # non-blocking wait
return {"id": id, "name": "Alice"}
# Run a coroutine
asyncio.run(fetch_user(1))
# await — suspend until coroutine completes
async def main():
user = await fetch_user(1)
print(user)
# Run concurrently — asyncio.gather (like Promise.all)
async def fetch_all():
results = await asyncio.gather(
fetch_user(1),
fetch_user(2),
fetch_user(3),
)
return results
# TaskGroup (Python 3.11+) — structured concurrency
async def fetch_with_group():
async with asyncio.TaskGroup() as tg:
task1 = tg.create_task(fetch_user(1))
task2 = tg.create_task(fetch_user(2))
# Both tasks complete before this line
return task1.result(), task2.result()
# Timeout
async def with_timeout():
try:
result = await asyncio.wait_for(slow_operation(), timeout=5.0)
except asyncio.TimeoutError:
print("operation timed out")
# Async generator
async def paginate(url: str):
cursor = None
while True:
data = await fetch_page(url, cursor)
for item in data["items"]:
yield item
cursor = data.get("next")
if not cursor:
break
async def consume():
async for item in paginate("/api/items"):
print(item)
# Async context manager
async def managed():
async with aiohttp.ClientSession() as session:
async with session.get("https://example.com") as resp:
return await resp.text()
# Queue — async producer-consumer (closest to Go channels)
async def producer(q: asyncio.Queue):
for i in range(10):
await q.put(i)
await q.put(None) # sentinel
async def consumer(q: asyncio.Queue):
while (item := await q.get()) is not None:
print(f"processing {item}")
q.task_done()
async def pipeline():
q: asyncio.Queue[int | None] = asyncio.Queue(maxsize=5)
await asyncio.gather(producer(q), consumer(q))
# Threading — true parallel execution (GIL limits CPU parallelism for pure Python)
import threading
def worker(name: str):
print(f"Thread {name} starting")
t = threading.Thread(target=worker, args=("Alice",))
t.start()
t.join()
# concurrent.futures — high-level threading/multiprocessing
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
with ThreadPoolExecutor(max_workers=4) as executor:
futures = [executor.submit(fetch_user, id) for id in range(10)]
results = [f.result() for f in futures]
# Multiprocessing — true CPU parallelism (bypasses GIL)
with ProcessPoolExecutor(max_workers=4) as executor:
results = list(executor.map(cpu_intensive, data))Key differences:
- Go goroutines are M:N green threads that run truly in parallel on multiple cores. Python's
asynciois single-threaded cooperative multitasking — only one coroutine runs at a time.- Python's GIL (Global Interpreter Lock) limits true CPU-bound parallelism in threads. Use
multiprocessingorProcessPoolExecutorfor CPU-bound work.- Go channels are language primitives. Python uses
asyncio.Queuefor async producer-consumer patterns.- Go 1.21+ has
sync.Map, channels, andselect. Python hasasyncio.Queue,asyncio.Event,asyncio.Lock,asyncio.Semaphore.
Sources: