Converting Python to Go
Introduction
One of the most effective ways to learn Go is to take Python code you already understand and convert it to Go. This reveals the structural differences between the two languages directly, using familiar problems as the starting point.
In this chapter you will use GitHub Copilot's chat to perform conversions and then study what changed. You will also convert code manually to build your own intuition before checking with Copilot.
Key patterns you will see repeatedly:
- Python's
for x in collection→ Go'sfor _, x := range collection - Python's
def f(x):→ Go'sfunc f(x T) R - Python's
try/except→ Go'sif err != nil - Python's
class→ Go'sstruct+ methods - Python's built-ins (
sum,max,filter,map) → Go's explicit loops
GitHub Copilot tip: Select a block of Python code, open Copilot Chat, and ask: Convert this Python code to idiomatic Go and explain the key differences.
Concepts
Syntax comparison
| Python | Go |
|---|---|
def greet(name): | func greet(name string) string |
return x, y | return x, y (both support multiple returns, but Go requires declared types) |
[x for x in lst if x > 0] | for loop with append |
d = {} | d := make(map[K]V) |
raise ValueError("msg") | return fmt.Errorf("msg") |
try: ... except: ... | if err != nil { ... } |
class Foo: | type Foo struct { ... } |
self.method() | f.Method() (receiver syntax) |
import os | import "os" |
Type differences
| Python | Go |
|---|---|
int (arbitrary precision) | int, int64 (fixed size) |
float (64-bit) | float64 |
str | string |
list | []T (typed) |
dict | map[K]V (typed) |
None | nil |
True / False | true / false |
Examples
# Python
def calculate_stats(numbers):
total = sum(numbers)
average = total / len(numbers)
return total, average, max(numbers), min(numbers)
// Go
func calculateStats(numbers []int) (total int, average float64, max int, min int) {
max, min = numbers[0], numbers[0]
for _, n := range numbers {
total += n
if n > max { max = n }
if n < min { min = n }
}
average = float64(total) / float64(len(numbers))
return
}
Key differences: Go has no sum(), max(), min() built-ins for slices — you loop manually. Go requires explicit float64() conversion for division. Go declares return type names upfront.
Challenges
1. Convert a list comprehension
- Here is a Python list comprehension:
squares = [x * x for x in range(1, 11)]
- Convert it to Go without using Copilot first. Then ask Copilot to check your solution.
💡 Show Answer
package main
import "fmt"
func main() {
squares := make([]int, 0, 10)
for i := 1; i <= 10; i++ {
squares = append(squares, i*i)
}
fmt.Println(squares)
}
Go has no list comprehensions — you always use explicit loops. Copilot can generate this instantly, but writing it yourself first builds the muscle memory.
2. Convert a dictionary operation
- Here is a Python program that counts character frequency:
def char_frequency(s):
freq = {}
for ch in s:
freq[ch] = freq.get(ch, 0) + 1
return freq
print(char_frequency("hello world"))
- Ask Copilot to convert it to Go. Study the result, then run it.
💡 Show Answer
package main
import "fmt"
func charFrequency(s string) map[rune]int {
freq := make(map[rune]int)
for _, ch := range s {
freq[ch]++
}
return freq
}
func main() {
result := charFrequency("hello world")
for ch, count := range result {
fmt.Printf("%c: %d\n", ch, count)
}
}
Note: Go iterates over strings with range and returns rune values (Unicode code points), not bytes. freq[ch]++ works because Go maps return the zero value (0) for missing keys — equivalent to Python's dict.get(key, 0).
3. Convert a class
- Here is a Python class:
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
def __str__(self):
return f"{self.owner}: ${self.balance:.2f}"
- Convert it to a Go struct with methods. The
withdrawmethod should return an error instead of raising an exception.
💡 Show Answer
package main
import (
"fmt"
)
type BankAccount struct {
Owner string
Balance float64
}
func (a *BankAccount) Deposit(amount float64) {
a.Balance += amount
}
func (a *BankAccount) Withdraw(amount float64) error {
if amount > a.Balance {
return fmt.Errorf("insufficient funds: balance %.2f, withdrawal %.2f", a.Balance, amount)
}
a.Balance -= amount
return nil
}
func (a BankAccount) String() string {
return fmt.Sprintf("%s: $%.2f", a.Owner, a.Balance)
}
func main() {
account := &BankAccount{Owner: "Alice", Balance: 100}
account.Deposit(50)
fmt.Println(account)
err := account.Withdraw(200)
if err != nil {
fmt.Println("Error:", err)
}
err = account.Withdraw(30)
if err == nil {
fmt.Println(account)
}
}
4. Predict the Go equivalent
- Before running or asking Copilot, write down what you think the Go equivalent of this Python would be:
def is_palindrome(s):
s = s.lower().replace(" ", "")
return s == s[::-1]
print(is_palindrome("racecar"))
print(is_palindrome("hello"))
print(is_palindrome("A man a plan a canal Panama"))
- Then ask Copilot to convert it. How close were you?
💡 Show Answer
package main
import (
"fmt"
"strings"
"unicode"
)
func isPalindrome(s string) bool {
s = strings.ToLower(strings.ReplaceAll(s, " ", ""))
runes := []rune(s)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
if runes[i] != runes[j] {
return false
}
}
return true
}
func main() {
fmt.Println(isPalindrome("racecar")) // true
fmt.Println(isPalindrome("hello")) // false
fmt.Println(isPalindrome("A man a plan a canal Panama")) // true
}
// Note: we need the unicode import for a more robust version
var _ = unicode.IsLetter // suppress unused import warning in this snippet
Go has no slice reversal shorthand like s[::-1]. Instead, you compare characters from both ends, moving inward. Go also requires converting the string to []rune to handle multi-byte Unicode characters correctly.
5. Fix the converted code
- Ask Copilot to convert this Python code to Go:
def flatten(nested):
result = []
for item in nested:
if isinstance(item, list):
result.extend(flatten(item))
else:
result.append(item)
return result
print(flatten([1, [2, 3], [4, [5, 6]], 7]))
- The conversion will be more involved than previous exercises. Use
/explainto understand what Copilot produces.
💡 Show Answer
package main
import "fmt"
// In Go, we need a concrete type — we'll use interface{} (or any) for mixed-type slices
func flatten(nested []interface{}) []interface{} {
result := []interface{}{}
for _, item := range nested {
if inner, ok := item.([]interface{}); ok {
result = append(result, flatten(inner)...)
} else {
result = append(result, item)
}
}
return result
}
func main() {
input := []interface{}{1, []interface{}{2, 3}, []interface{}{4, []interface{}{5, 6}}, 7}
fmt.Println(flatten(input))
}
This highlights a key difference: Python lists can hold mixed types naturally. In Go, you must use interface{} (or any in Go 1.18+) for mixed-type collections, and you need a type assertion (item.([]interface{})) to check if an element is itself a slice. This is more verbose in Go — a sign that this kind of dynamic structure is not idiomatic Go.
6. When Python wins
- Ask Copilot: For which types of tasks is Python genuinely better than Go, even accounting for Go's performance advantage?
- Make a list of at least five use cases where you would still choose Python.
💡 Show Answer
Python is typically the better choice for:
- Data science and machine learning — NumPy, pandas, scikit-learn, PyTorch, and TensorFlow are all Python-first.
- Scientific computing — SciPy, Matplotlib, Jupyter notebooks.
- Quick scripting and automation — Python's brevity and large stdlib make it ideal for one-off scripts.
- Rapid prototyping — fewer lines of code to get something working.
- Glue code — connecting tools, parsing files, processing data in pipelines.
- NLP and text processing — NLTK, spaCy, and Hugging Face transformers.
- Dynamic or mixed-type data — Python handles heterogeneous structures naturally; Go requires
interface{}.