Calling Go Functions From Python

On September 28, I gave a lightning talk at /dev/reno about calling Go functions from Python. While I used Google Slides for the presentation, the slide content remained text (except the QR code on the final slide).
Calling Go From Python
This demonstrates how the slide content remained text-based.
Important Links
- The Github repository, bandarji/import-go-from-python, contains all the proof of concept source code.
- That repository also contains the actual slide source.
- The README explains all the content, in detail.
- You can also view the actual presentation slide deck.
The lightning talk discusses a proof of concept for calling functions written
in Go from Python. Python can load a compiled shared library with
ctypes, calling the functions exported from Go.
I used uv v0.12 for the virtual environment and packages, Python v3.14.7, Go to compile, and Git for the repository. Docker v29.6.2 ran the Linux build.
How To Compile A Shared Library

Within the Go package cgotopypoc, C code sits in a comment block, called the
CGO preamble, immediately before import "C". The go build command from the
slide demonstrates how to compile the shared library. That library’s file
extension depends on the operating system: so for Linux, dylib for Mac OS
and dll for Windows.
cd cgotopypoc
CGO_ENABLED=1 go build -buildmode=c-shared -o libcgotopypoc.so .
Go And Python Example Code
The CGO preamble, C code that sits as Go comments just before the
import "C" line, prepares the shared library. If exports pass a void
pointer to CGO, to cover Python’s None type, the Go code requires
import "unsafe".
On the Python side, ctypes.CDLL loads the shared library and each exported
name is a callable. In this repository, cgotopypoc.py runs the build, loads
the result and sets restype on each export before any assertion runs.
References:
Repository Layout
cgotopypoc/
go.mod # Go module
returns.go # CGO exports for each compatible Python return type
record.go # Go Record struct and its CGO exports
gonpypoc/
pyproject.toml # project metadata and build backend
uv.lock # locked dependency set
.python-version # pinned interpreter (v3.14)
src/gonpypoc/
__init__.py # package entry; runs the CGO assertions
cgotopypoc.py # builds, loads and asserts the shared library
tests/
conftest.py # shared pytest fixture that loads the CGO library
test_cgotopypoc.py # Unit tests (pytest)
test_go_returns.py # Unit tests (pytest)
A unit testing dependency, pytest, installed by uv, checks the
Go function responses. Access with make docker-test.
Supported Data Types

This slide displays many supported data types as values flow from Go to Python, crossing the C ABI.
- Complex numbers travel as a small C struct of two doubles.
Python rebuilds the value with
complex(real, imag). - Strings and byte slices come back as
char*. Python copies the bytes, then callsFreeCStringso the C allocation does not linger. The Unicode check returnsबंदरजी(bandarji, written in Hindi). Recordrepresents a struct. CGO will not put a Go struct in an//exportsignature (Go type not supported in export: struct), sorecord.gocopiesRecordto and fromCRecord, a C struct with the same field order and sizes. Python declares that layout as actypes.StructurenamedCRecordand reads the value as aRecordnamed tuple.
Go’s garbage collector does not track memory handed to CGO. Pointers can
cause issues here, as memory reclamation can occur. The gopy tool
addresses this by replacing pointers with int64 values, to reserve
memory untouchable by garbage collection.
CGO vs gopy

CGO ships with Go. Python reaches it through a shared library and ctypes.
Strings cross with C.CString. Structs are copied through a C struct.
Slices and maps have no C equivalent, so this design keeps them on the Go
side. Objects cross as C values and pointers. The go build command
compiles the shared library.
Not part of the standard Go installation, gopy generates extension
modules using the gopy build tool. This aligns Go function responses more
closely with Python. Strings arrive as str. Structs come in as Python
classes with fields and methods. Slices and maps have wrappers for Python
compatibility. Go’s garbage collector does not reclaim memory, as pointers
become int64 handles.
The shared library fits a small surface of numbers, strings, bytes, and plain structs. gopy fits a Go API made of slices, maps, and pointers.
The warning under the table is the part I want to keep. One Go repository, called from several languages, has real appeal. That appeal still has to survive the C boundary. Just because the shared library builds does not mean every Go API belongs on it.
Global Interpreter Lock

Python’s global interpreter lock (GIL) ensures that only one thread executes at a time. Python releases the GIL for the ctypes call into Go. Python threads sitting inside that call run at the same time, and the interpreter waits until they return.
Inside the Go function, new goroutines belong to the Go scheduler. The
scheduler spreads them across an OS thread pool capped by GOMAXPROCS.
Each Python thread that enters Go is also tracked by Go: one OS thread per Python thread, in addition to Go’s own pool. Those OS threads cost more memory than goroutines. In my humble opinion, I think Go should handle concurrency when leveraging CGO.
Supporting Data Types

Data types differ between these two languages. Python does not have strict
typing, while Go does. Yes, Go has generics available, but I do not
recommend pushing unknown data into Go functions. Yes, Python has
pydantic as a way to somewhat guardrail types, but that provides
cumbersome enforcement, which could end up overlooked or improperly
employed.
Python’s ctypes provides guardrails, if you add argtypes. Declare the C
types expected and ctypes validates arguments before performing function
calls. A string sent to a c_int export raises TypeError. C and Go never
see that call.
With argtypes unset, ctypes has no insight into expect function parameters.
Some mismatches come back as bad processing or unexpected output. Some crash.
Go may panic or the code may end up triggering a segmentation fault.
The pytest suite sets the argument and return types, then checks every value that comes back.
Thank You

The QR code points at the repository, github.com/bandarji/import-go-from-python. I thank /dev/reno for granting me a few minutes to present this content. Looking forward to giving another talk on a different topic at some point in the near future.