Skip to main content

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's for _, x := range collection
  • Python's def f(x): → Go's func f(x T) R
  • Python's try/except → Go's if err != nil
  • Python's class → Go's struct + 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​

PythonGo
def greet(name):func greet(name string) string
return x, yreturn 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 osimport "os"

Type differences​

PythonGo
int (arbitrary precision)int, int64 (fixed size)
float (64-bit)float64
strstring
list[]T (typed)
dictmap[K]V (typed)
Nonenil
True / Falsetrue / 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 withdraw method 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 /explain to 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:

  1. Data science and machine learning — NumPy, pandas, scikit-learn, PyTorch, and TensorFlow are all Python-first.
  2. Scientific computing — SciPy, Matplotlib, Jupyter notebooks.
  3. Quick scripting and automation — Python's brevity and large stdlib make it ideal for one-off scripts.
  4. Rapid prototyping — fewer lines of code to get something working.
  5. Glue code — connecting tools, parsing files, processing data in pipelines.
  6. NLP and text processing — NLTK, spaCy, and Hugging Face transformers.
  7. Dynamic or mixed-type data — Python handles heterogeneous structures naturally; Go requires interface{}.