Python for Go Developers
Python is the lingua franca of AI engineering. From PyTorch and TensorFlow to LangChain and LlamaIndex, every major framework in the AI ecosystem is built in Python. If you come from a Go background, you already have strong engineering instincts β compile-time discipline, explicit error handling, and a bias toward simplicity. You just need to learn the new vocabulary and tooling.
This guide bridges that gap. By the end, you will understand how your Go knowledge maps to the Python world and be ready to contribute to our AI engineering projects.
This guide assumes you are comfortable with Go and want to learn Python specifically for AI engineering. If you are new to Python entirely, also read the Python Getting Started guide which covers the tooling stack in more depth.
Why Python for AI Engineeringβ
Go is an excellent language. Its compile speed, concurrency model, and simplicity are real strengths. But the AI/ML ecosystem is overwhelmingly Python-native. Here is why:
- Framework dominance. PyTorch, TensorFlow, JAX, LangChain, LlamaIndex, Hugging Face Transformers β all first-class Python APIs.
- Data science stack. NumPy, Pandas, Polars, scikit-learn form the foundation of data manipulation and analysis.
- Rapid prototyping. Python's dynamic typing and REPL-friendly nature make it ideal for experimentation and iteration.
- Community and documentation. When you encounter an error in an AI library, the top search results will almost always be Python-centric.
- Model interoperability. The Hugging Face model hub, ONNX, and most model serialization formats are Python-first.
This does not mean Python replaces Go in your toolkit. Many production systems use a hybrid approach: Go for high-throughput services and APIs, Python for AI/ML workloads. But you need to be fluent in both.
The Mental Model: Go β Python Mappingβ
The fastest way to learn Python is to understand what corresponds to the tools and concepts you already know. The following table maps your Go ecosystem to the Python equivalents we use on this team.
| Go Concept | Python Equivalent | Notes |
|---|---|---|
go mod | uv | uv is our package manager β orders of magnitude faster than pip |
go.mod | pyproject.toml | Single file for metadata, dependencies, and tool configuration |
go.sum | uv.lock | Deterministic lockfile for reproducible builds |
go run | uv run | Execute code with dependencies resolved |
go get | uv add | Add a dependency to your project |
go install | uv tool install / uvx | Install or run tools without project dependency |
golangci-lint | ruff | One tool replaces linting and formatting, 10β100Γ faster |
gofmt | ruff format | Opinionated code formatting |
go test | pytest | The de facto Python test runner |
go vet | ty | Static type checker (10β100Γ faster than mypy) |
interface{} | typing.Any | Dynamic / untyped values |
struct | @dataclass / Pydantic model | Structured data with auto-generated methods |
goroutines | asyncio / async functions | Cooperative concurrency model |
context.Context | Dependency injection / config passing | Request-scoped state management |
if err != nil | try / except | Exception-based error handling |
channels | asyncio.Queue | Thread-safe message passing |
sync.WaitGroup | asyncio.gather() | Wait for multiple coroutines |
$GOPATH / module cache | .venv/ | Isolated virtual environment per project |
go build | uv build (or just uv sync) | Build and install artifacts |
The biggest conceptual shift from Go is not syntax β it is error handling and typing. Go forces you to check every error at the call site. Python uses exceptions, which bubble up the stack. Go has compile-time type safety. Python has optional type hints checked by a separate tool (ty). Both approaches work; you just need to adapt your mental model.
Setting Up Your Environment with uvβ
What is uv?β
uv is an extremely fast Python package and project manager written in Rust by Astral. It replaces pip, virtualenv, pip-tools, and more β all while being orders of magnitude faster. It is the single most important tool in the Python workflow for our team.
Think of uv as the go mod + go run + go install of the Python world, but faster.
Installing uvβ
- macOS / Linux (standalone)
- macOS (Homebrew)
- Windows
curl -LsSf https://astral.sh/uv/install.sh | sh
brew install uv
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
Verify the installation:
$ uv --version
uv 0.x.x
Your First Project: go mod vs uvβ
Here is the complete project setup workflow side by side:
- Go workflow
- uv workflow
# 1. Initialize module
go mod init github.com/org/my-project
# 2. Add dependencies
go get github.com/gin-gonic/gin
go get github.com/stretchr/testify
# 3. Download and vendor
go mod tidy
go mod vendor
# 4. Run the project
go run cmd/server/main.go
# 5. Install a tool globally
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
# 1. Initialize project (creates pyproject.toml + .python-version)
uv init my_project && cd my_project
# 2. Add dependencies
uv add fastapi
uv add --dev ruff ty pytest
# 3. Sync environment (creates .venv/ and uv.lock)
uv sync
# 4. Run the project
uv run src/my_project/main.py
# 5. Install a tool globally
uv tool install ruff
Understanding the Project Filesβ
After running uv init, your project looks like this:
my_project/
βββ .git/
βββ .gitignore
βββ .python-version # Pinned Python version (e.g., "3.12")
βββ .venv/ # Virtual environment (created on first uv sync/run)
β βββ bin/
β βββ lib/
β βββ pyvenv.cfg
βββ src/
β βββ my_project/ # Your package
β βββ __init__.py
β βββ main.py
βββ pyproject.toml # Project metadata + dependencies
βββ uv.lock # Lockfile (like go.sum)
The pyproject.toml is the heart of your project configuration:
[project]
name = "my_project"
version = "0.1.0"
description = "My AI engineering project"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"fastapi",
"pydantic",
]
[tool.uv.dev-dependencies]
ruff = ">=0.4.0"
ty = ">=0.0.1"
pytest = ">=8.0.0"
| Go command | uv equivalent |
|---|---|
go mod init | uv init |
go get <pkg> | uv add <pkg> |
go mod tidy | uv lock (or uv sync) |
go run ./cmd/... | uv run <script> |
go install <pkg>@latest | uv tool install <pkg> |
go env GOPATH | uv venv (creates .venv/) |
go list -m all | uv tree |
go mod graph | uv lock --show-resolved |
Python Syntax Crash Courseβ
This section highlights the key differences between Go and Python. It is not a complete Python tutorial β for that, see the official Python tutorial. Instead, it focuses on what will trip you up coming from Go.
Variables and Type Hintsβ
Go is statically typed. Python is dynamically typed, but you can (and should) add type hints. Think of type hints as compile-time annotations that a separate tool (ty) checks.
- Go
- Python
package main
var name string = "Alice"
var age int = 30
var isActive bool = true
var tags []string = []string{"ai", "python"}
var config map[string]any = map[string]any{"key": "value"}
// Shorthand declaration (local scope only)
// name := "Alice"
// age := 30
name: str = "Alice"
age: int = 30
is_active: bool = True
tags: list[str] = ["ai", "python"]
config: dict[str, object] = {"key": "value"}
# Type hints are optional at runtime.
# They exist for static analysis, checked by ty.
Key differences:
- No
var/:=. Python has no variable declaration keyword. Assignment is declaration. nullβNone. Python has a single null value:None.true/falseβTrue/False. Note the capitalization.interface{}βobjectortyping.Any. For untyped values.- Type hints are not enforced at runtime. They exist for static analysis only (
tychecks them). - No implicit type inference at package level. In Go,
var x = 42infersint. In Python,x = 42is dynamically typed; add: intfor static checking.
Functions and Return Typesβ
- Go
- Python
package main
import "fmt"
// Named return values with error handling
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
// Multiple return values
func minMax(nums []int) (int, int) {
min, max := nums[0], nums[0]
for _, n := range nums {
if n < min {
min = n
}
if n > max {
max = n
}
}
return min, max
}
from typing import NoReturn
def divide(a: float, b: float) -> float:
"""Divide a by b. Raises ValueError if b is zero."""
if b == 0:
raise ValueError("division by zero")
return a / b
# Multiple return values as a tuple
def min_max(nums: list[int]) -> tuple[int, int]:
min_val, max_val = nums[0], nums[0]
for n in nums:
if n < min_val:
min_val = n
if n > max_val:
max_val = n
return min_val, max_val
Key differences:
funcβdef. Python uses thedefkeyword.- No named return values. Python functions return a single value or a tuple.
- Error handling is via exceptions, not return values. Instead of
(result, error), youraisean exception and the callertry/excepts it. - Return types. The arrow
->specifies the return type, placed after the parameter list. - Indentation matters. Python uses whitespace (4 spaces) for block structure, not curly braces.
Structs vs Dataclassesβ
Go's struct is the workhorse of data representation. Python's closest equivalent is the @dataclass decorator.
- Go
- Python
package main
import "fmt"
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Role string `json:"role"`
}
func NewUser(id int, name, email, role string) *User {
return &User{
ID: id,
Name: name,
Email: email,
Role: role,
}
}
func (u *User) DisplayName() string {
return fmt.Sprintf("%s (%s)", u.Name, u.Role)
}
from dataclasses import dataclass
from typing import Literal
@dataclass
class User:
id: int
name: str
email: str
role: Literal["admin", "user", "moderator"]
def display_name(self) -> str:
return f"{self.name} ({self.role})"
# Creating instances
user = User(id=1, name="Alice", email="alice@example.com", role="admin")
Key differences:
type X structβ@dataclass class X. Dataclasses auto-generate__init__,__repr__, and__eq__methods from type annotations.selfis explicit. Every instance method takesselfas the first parameter.- No pointer semantics. Python variables are references. There is no
*UservsUserdistinction. Literalfor constrained strings. Instead of checking at runtime, useLiteral["admin", "user"]for compile-time safety.- Struct tags β Pydantic. For JSON serialization with field aliases, use Pydantic models instead of struct tags.
Error Handlingβ
This is the biggest mental shift. Go's if err != nil pattern is explicit and local. Python's exception model is implicit and stack-wide.
- Go
- Python
package main
import (
"database/sql"
"fmt"
"log"
)
func getUser(db *sql.DB, id int) (*User, error) {
row := db.QueryRow("SELECT id, name, email FROM users WHERE id = $1", id)
var u User
err := row.Scan(&u.ID, &u.Name, &u.Email)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("user %d: not found", id)
}
return nil, fmt.Errorf("user %d: %w", id, err)
}
return &u, nil
}
func main() {
db, err := sql.Open("postgres", "dsn")
if err != nil {
log.Fatal(err)
}
defer db.Close()
user, err := getUser(db, 42)
if err != nil {
log.Printf("failed to get user: %v", err)
return
}
fmt.Println(user)
}
import sqlite3
from contextlib import contextmanager
class UserNotFoundError(Exception):
"""Raised when a user is not found."""
def __init__(self, user_id: int) -> None:
self.user_id = user_id
super().__init__(f"user {user_id}: not found")
@contextmanager
def get_db() -> sqlite3.Connection:
db = sqlite3.connect("app.db")
try:
yield db
finally:
db.close()
def get_user(db: sqlite3.Connection, id: int) -> dict:
cursor = db.execute(
"SELECT id, name, email FROM users WHERE id = ?", (id,)
)
row = cursor.fetchone()
if row is None:
raise UserNotFoundError(id)
return {"id": row[0], "name": row[1], "email": row[2]}
def main() -> None:
with get_db() as db:
try:
user = get_user(db, 42)
print(user)
except UserNotFoundError as e:
print(f"failed to get user: {e}")
Key differences:
if err != nilβtry/except. Errors are exceptions that propagate up the call stack.deferβwith(context managers). Python'swithstatement handles cleanup automatically, similar todefer.- Custom error types β Custom exception classes. Define subclasses of
Exceptionfor domain-specific errors. fmt.Errorf("...: %w", err)βraise ... from err. Python supports exception chaining withfrom.- No error wrapping syntax. Use exception chaining (
raise NewError from original) instead of%w.
Python follows the "Easier to Ask Forgiveness than Permission" (EAFP) philosophy. Go follows "Look Before You Leap" (LBYL). In Go, you check for errors before proceeding. In Python, you try the operation and catch the exception if it fails. Both are valid; Python's approach leads to less boilerplate in common cases.
Slices/Maps vs Lists/Dictsβ
- Go
- Python
package main
import "fmt"
func main() {
// Slices
names := []string{"Alice", "Bob", "Charlie"}
names = append(names, "Diana")
fmt.Println(names[1:3]) // [Bob Charlie]
// Maps
ages := map[string]int{
"Alice": 30,
"Bob": 25,
}
ages["Charlie"] = 35
if age, ok := ages["Alice"]; ok {
fmt.Printf("Alice is %d\n", age)
}
// Range over map
for name, age := range ages {
fmt.Printf("%s is %d\n", name, age)
}
}
# Lists
names: list[str] = ["Alice", "Bob", "Charlie"]
names.append("Diana")
print(names[1:3]) # ['Bob', 'Charlie']
# Dicts
ages: dict[str, int] = {
"Alice": 30,
"Bob": 25,
}
ages["Charlie"] = 35
if name in ages:
print(f"Alice is {ages['Alice']}")
# Iterate over dict
for name, age in ages.items():
print(f"{name} is {age}")
Key differences:
[]Tβlist[T]. Python lists are dynamically sized (like Go slices, not arrays).map[K]Vβdict[K, V]. Python dicts are hash maps, equivalent to Go maps.okidiom βinoperator. Python useskey in dictinstead ofval, ok := m[key].appendβ.append(). Python uses method syntax instead of theappendbuilt-in.- No
make(). Python creates empty collections with[],{}, orlist(),dict().
Interfaces vs Protocolsβ
Go's interfaces are structural and implicit. Python has typing.Protocol for the same concept.
- Go
- Python
package main
import "fmt"
// Interface satisfied implicitly (duck typing)
type Stringer interface {
String() string
}
type User struct {
Name string
}
func (u *User) String() string {
return u.Name
}
type Product struct {
Title string
}
func (p *Product) String() string {
return p.Title
}
// Accepts any type that implements Stringer
func describe(s Stringer) {
fmt.Println(s.String())
}
func main() {
describe(&User{Name: "Alice"})
describe(&Product{Title: "Widget"})
}
from typing import Protocol
# Protocol for structural typing (duck typing)
class Stringer(Protocol):
def string(self) -> str: ...
class User:
def __init__(self, name: str) -> None:
self.name = name
def string(self) -> str:
return self.name
class Product:
def __init__(self, title: str) -> None:
self.title = title
def string(self) -> str:
return self.title
# Accepts any type that has a string() method
def describe(s: Stringer) -> None:
print(s.string())
describe(User("Alice"))
describe(Product("Widget"))
Key differences:
- Both are structural. Neither Go interfaces nor Python protocols require explicit
implementsdeclarations. interface{}βProtocolortyping.Any. For fully dynamic values, useAny. For structural contracts, useProtocol.- Method naming. Go uses
String()(capitalized). Python usesstring()(lowercase,snake_case). fmt.Stringerβ__str__. Python has a built-in string representation method (__str__) that is called automatically byprint().
Goroutines/Channels vs asyncioβ
- Go
- Python
package main
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for j := range jobs {
results <- j * j
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, jobs, results, &wg)
}
// Send jobs
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
// Wait for workers
wg.Wait()
close(results)
// Collect results
for r := range results {
fmt.Println(r)
}
}
import asyncio
async def worker(id: int, queue: asyncio.Queue[int], results: list[int]) -> None:
while True:
try:
job = queue.get_nowait()
except asyncio.QueueEmpty:
break
result = job * job
results.append(result)
queue.task_done()
async def main() -> None:
queue: asyncio.Queue[int] = asyncio.Queue()
results: list[int] = []
# Put jobs in the queue
for j in range(1, 6):
await queue.put(j)
# Run workers concurrently
await asyncio.gather(
worker(1, queue, results),
worker(2, queue, results),
worker(3, queue, results),
)
# Collect results
for r in sorted(results):
print(r)
asyncio.run(main())
Key differences:
go func()βasyncio.create_task()orasyncio.gather(). Python does not have lightweight OS threads; it uses cooperative coroutines.chanβasyncio.Queue. Python's async queue provides similar functionality but is cooperative, not preemptive.sync.WaitGroupβasyncio.gather(). Wait for multiple coroutines to complete.close(chan)β queue exhaustion. Python queues do not have acloseprimitive; you signal completion differently.- True parallelism vs cooperative. Go goroutines run on OS threads with true parallelism. Python asyncio runs on a single thread with cooperative scheduling.
See the Concurrency section below for a deeper comparison.
Project Structureβ
Python projects follow a different convention than Go projects. Understanding the layout is critical for knowing where to put code and how imports work.
- Typical Go Project
- Typical Python Project
my-project/
βββ go.mod
βββ go.sum
βββ cmd/
β βββ server/
β βββ main.go # Entry point
βββ internal/
β βββ handler/
β β βββ user_handler.go
β βββ service/
β β βββ ai_service.go
β βββ model/
β βββ user.go
βββ pkg/
β βββ utils/
β βββ helpers.go
βββ tests/
βββ user_handler_test.go
my_project/
βββ pyproject.toml # Dependencies + tool config
βββ uv.lock # Lockfile (like go.sum)
βββ .python-version # Pinned Python version
βββ .venv/ # Virtual environment (gitignored)
βββ src/
β βββ my_project/ # Package directory
β βββ __init__.py # Makes it a package
β βββ main.py # Entry point
β βββ handlers/
β β βββ __init__.py
β β βββ user_handler.py
β βββ services/
β β βββ __init__.py
β β βββ ai_service.py
β βββ models/
β βββ __init__.py
β βββ user.py
βββ tests/
βββ __init__.py
βββ test_user_handler.py
Key Structural Differencesβ
cmd/βsrc/layout. Go separates entry points intocmd/. Python uses asrc/layout where the package lives undersrc/<package_name>/.internal/β package privacy. Go enforces internal package privacy at the compiler level. Python has no such enforcement; use naming conventions (leading underscore) and project structure.pkg/β top-level modules. Go'spkg/for shared libraries maps to top-level modules within the Python package.go.modat root βpyproject.tomlat root. Both live at the project root.- No
go.sumequivalent in source. Theuv.lockfile is the lockfile, analogous togo.sum.
__init__.py β No Go Equivalentβ
Every directory that contains Python code needs an __init__.py file (which can be empty). This is Python's way of saying "this directory is a package."
__init__.py ExplainedIn Go, any directory with .go files is implicitly part of a package. In Python, the __init__.py file is the marker that makes a directory importable. It can be empty, or it can contain code that runs when the package is imported (similar to a package-level init() function, but Python does not have init()).
Import Pathsβ
In Go, you import using module paths:
import (
"github.com/org/my-project/internal/handler"
"github.com/org/my-project/internal/service"
)
In Python, you import using package names relative to the package root:
from my_project.handlers.user_handler import handle_user
from my_project.services.ai_service import AIService
The import path starts from the package root (the directory containing __init__.py), not from the current file. This is one of the most common sources of confusion for Go developers.
Python imports are relative to the Python path (sys.path), not the file system. When you run uv run src/my_project/main.py, the src/ directory is on the path, so you import my_project.something, not src.my_project.something. This differs from Go where the module path is explicit in go.mod.
Our Tooling Stackβ
Our team uses three tools from the Astral ecosystem. Together, they replace what would be multiple separate tools in the Go world.
ruff β Linting and Formattingβ
ruff is an extremely fast Python linter and code formatter written in Rust. It replaces Flake8, isort, Black, pydocstyle, pyupgrade, autoflake, and dozens of plugins β all while running 10β100Γ faster.
In Go terms, ruff replaces both golangci-lint and gofmt.
Key commands:
# Lint the project (like golangci-lint run)
uvx ruff check .
# Auto-fix fixable issues
uvx ruff check --fix .
# Format the project (like gofmt -w)
uvx ruff format .
# Lint and format in one go
uvx ruff check --fix . && uvx ruff format .
Configuration lives in pyproject.toml:
[tool.ruff]
target-version = "py312"
line-length = 88
[tool.ruff.lint]
select = ["ALL"]
ignore = [
"D100", # Missing docstring on module
"D104", # Missing docstring on public package
]
[tool.ruff.lint.pydocstyle]
convention = "google"
In Go, you run golangci-lint run for linting and gofmt -w for formatting. With ruff, one tool does everything with over 900 built-in rules, including import sorting (replaces goimports), docstring checking, and modernization.
ty β Type Checkingβ
ty is an extremely fast Python type checker and language server written in Rust. It is 10β100Γ faster than mypy and Pyright, making it practical to run on every edit.
In Go terms, ty is the closest equivalent to go vet for type-level checks, but it operates on optional type hints rather than a fully static type system.
Key commands:
# Type check the project (like go vet)
uvx ty check
# Type check a single file
uvx ty check src/my_project/main.py
Configuration in pyproject.toml:
[tool.ty]
python-version = "3.12"
go vet finds suspicious constructs at compile time because Go is statically typed. ty finds type errors in type hints at analysis time because Python is dynamically typed. The key difference: Go's type system is mandatory and enforced at compile time. Python's type hints are optional and checked by a separate tool.
Pre-commit Hooksβ
We wire ruff and ty into pre-commit hooks so that quality checks run automatically before every commit. A typical .pre-commit-config.yaml:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/astral-sh/ty
rev: v0.0.33
hooks:
- id: ty
Install the hooks:
uv add --dev pre-commit
uv run pre-commit install
After installation, ruff and ty run automatically on staged files whenever you run git commit.
Concurrency: goroutines vs asyncioβ
This is the deepest conceptual shift. Go's concurrency model is based on CSP (Communicating Sequential Processes) with true parallelism. Python's asyncio is cooperative concurrency on a single thread.
Concept Mappingβ
| Go Concept | Python asyncio Equivalent | Notes |
|---|---|---|
go func() | asyncio.create_task() | Schedule a coroutine for execution |
chan | asyncio.Queue | Async-safe message passing |
sync.WaitGroup | asyncio.gather() | Wait for multiple coroutines |
select | asyncio.wait() / asyncio.selectors | Multiplex across async operations |
context.Context | asyncio.Task cancellation / asyncio.timeout | Timeout and cancellation |
sync.Mutex | asyncio.Lock | Mutual exclusion for async code |
errgroup | asyncio.gather() with return_exceptions=True | Run coroutines with error aggregation |
Spawning Concurrent Workβ
- Go
- Python
package main
import (
"context"
"fmt"
"sync"
"time"
)
func fetch(ctx context.Context, url string, result *string, wg *sync.WaitGroup) {
defer wg.Done()
// Simulated work
time.Sleep(100 * time.Millisecond)
*result = fmt.Sprintf("data from %s", url)
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var wg sync.WaitGroup
var result1, result2 string
wg.Add(2)
go fetch(ctx, "https://api.example.com/users", &result1, &wg)
go fetch(ctx, "https://api.example.com/posts", &result2, &wg)
wg.Wait()
fmt.Println(result1, result2)
}
import asyncio
async def fetch(url: str) -> str:
# Simulated async work
await asyncio.sleep(0.1)
return f"data from {url}"
async def main() -> None:
try:
result1, result2 = await asyncio.gather(
fetch("https://api.example.com/users"),
fetch("https://api.example.com/posts"),
return_exceptions=False,
)
print(result1, result2)
except asyncio.TimeoutError:
print("request timed out")
asyncio.run(main())
Channels and Message Passingβ
- Go
- Python
package main
import "fmt"
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}
func main() {
ch := make(chan int)
go producer(ch)
for val := range ch {
fmt.Println(val)
}
}
import asyncio
async def producer(queue: asyncio.Queue[int]) -> None:
for i in range(5):
await queue.put(i)
await queue.put(None) # Sentinel to signal done
async def main() -> None:
queue: asyncio.Queue[int | None] = asyncio.Queue()
asyncio.create_task(producer(queue))
while True:
val = await queue.get()
if val is None:
break
print(val)
asyncio.run(main())
When to Use Eachβ
- Go goroutines run on OS threads managed by the Go runtime scheduler. They achieve true parallelism and are ideal for CPU-bound and I/O-bound work.
- Python asyncio runs coroutines on a single thread with cooperative scheduling. It is ideal for I/O-bound work (network calls, file I/O) but does not help with CPU-bound tasks.
- GIL. Python's Global Interpreter Lock means only one thread executes Python bytecode at a time. For CPU-bound parallelism in Python, use
multiprocessingor offload to C extensions (which NumPy, PyTorch, etc. already do). - Cancellation. Go's
context.Contextis deeply integrated into the standard library. Python'sasynciohas cancellation viatask.cancel()andasyncio.timeout(), but it is not as pervasive.
Your First Day Checklistβ
Here is a practical checklist to get you productive on day one:
- Install uv β
curl -LsSf https://astral.sh/uv/install.sh | sh - Clone the repository β
git clone <repo-url> && cd <repo-name> - Sync dependencies β
uv sync(creates.venv/and installs everything) - Run the linter β
uvx ruff check .(likegolangci-lint run) - Run the formatter β
uvx ruff format .(likegofmt -w) - Run the type checker β
uvx ty check(likego vet) - Run the tests β
uv run pytest(likego test ./...) - Install pre-commit hooks β
uv run pre-commit install(automates steps 4β6 on every commit)
After completing the checklist, make a small change β add a type hint, fix a lint warning, or write a test. Commit it and let the pre-commit hooks validate your work. This is your "Hello, World" of the Python workflow.
Key Takeawaysβ
- uv replaces go mod. It manages packages, virtual environments, and lockfiles β all in one extremely fast tool.
- pyproject.toml replaces go.mod. It holds project metadata, dependencies, and tool configuration in a single file.
- Type hints + ty replace compile-time checking. Python type hints give you static analysis without sacrificing dynamic flexibility.
- ruff replaces golangci-lint + gofmt. One tool handles linting, formatting, and import sorting with 900+ rules.
.venv/replaces the Go module cache. Each project gets an isolated virtual environment.uvxreplaces go install for tools. Run tools without global installation.- Exceptions replace
if err != nil. Python uses exception-based error handling instead of explicit error returns. @dataclassreplacesstruct. Dataclasses auto-generate constructors and methods from type annotations.asyncioreplaces goroutines for I/O concurrency. Python's async model is cooperative, not preemptive.__init__.pyhas no Go equivalent. It marks directories as importable packages.
Further Readingβ
- uv Documentation β Official uv guides and reference
- ruff Documentation β Official ruff guides and rule catalog
- ty Documentation β Official ty documentation
- Python 3 Documentation β Official Python language reference and tutorial
- Static Typing with Python β Comprehensive guide to Python type hints
- asyncio Documentation β Official asyncio guide
- Go Proverbs β Remember: "Don't communicate by sharing memory; share memory by communicating" β Python's asyncio follows a similar philosophy with queues
- [A Note About Python and Threads](https://docs.python.org/3/library/ threading.html) β Understanding the GIL and why asyncio exists