Leaving the Safety Net
High-level languages like Python, Go, and Java provide safety guarantees: automatic memory management (garbage collection), bounds checking on arrays, type safety, and meaningful error messages when things go wrong. These guarantees exist because the language runtime controls all memory allocation and deallocation.
When you cross the FFI boundary, you step outside that safety net. The C or Rust code on the other side operates under different rules, and the Python runtime cannot protect you from mistakes made there.
The Three Failure Modes
1. Memory leaks
Python's garbage collector tracks every Python object and frees memory when objects are no longer referenced. But if C code allocates memory and returns a pointer to Python, the garbage collector has no knowledge of that allocation. It cannot free it. If your Python code does not explicitly call a corresponding C free() function, that memory is leaked — permanently consumed until the process exits.
# Dangerous pattern: C allocates memory, Python receives a pointer
result_ptr = lib.get_result_buffer() # C malloc'd this
# ... use result_ptr ...
# If you forget this line, the memory leaks:
lib.free_result_buffer(result_ptr)
2. Segmentation faults (segfaults)
A segfault occurs when a program tries to access memory it does not own. In pure Python, this is nearly impossible — the runtime validates all memory accesses. Across FFI, it is easy to trigger. If Python passes an integer where C expects a pointer, C will interpret that integer as a memory address and try to read from it. That address almost certainly points to memory the process does not own. The OS kills the process immediately with SIGSEGV. There is no exception to catch, no stack trace to debug — just a crash.
3. Type mismatches and data corruption
C's int is typically 32 bits. Python's int is arbitrary precision. If you pass a large Python integer to a C function expecting a 32-bit int without explicit conversion, the value silently truncates. The function runs, returns a result, and the bug is invisible until a downstream system produces wrong output.
When FFI Is Worth It
FFI is the right tool in three specific situations:
- Reusing an existing native library that has no Python equivalent (e.g., a hardware driver, a proprietary SDK, a specialized codec).
- Performance-critical inner loops where even optimized Python is too slow and the overhead of a network call is unacceptable (e.g., real-time signal processing, cryptographic operations).
- Wrapping a battle-tested C/C++ library where rewriting in Python would introduce new bugs (e.g., wrapping OpenSSL rather than reimplementing TLS in Python).
For everything else — service-to-service communication, data transformation, business logic — use network APIs (gRPC or REST). The safety guarantees and operational simplicity of network-based communication outweigh the performance cost in the vast majority of cases.
If an interviewer asks "how does NumPy achieve its performance in Python?", a shallow answer is "it uses C under the hood." A complete answer explains the mechanism: NumPy is a C extension module. When you call numpy.dot(), Python's C extension API (a formalized FFI) calls a compiled C function directly, passing a pointer to the array's raw memory buffer. The C code operates on contiguous memory with SIMD CPU instructions, bypassing Python's interpreter entirely. The result is returned as a Python object. This is why NumPy operations on large arrays are orders of magnitude faster than equivalent Python loops — the loop itself runs in C, not in the Python interpreter.
Key Point: The decision between FFI and a network API is fundamentally a trade-off between performance and operational safety. FFI gives you zero-copy, zero-serialization function calls at the cost of manual memory management and crash risk. Network APIs give you process isolation (a crash in the downstream service does not crash your service), independent deployability, and language-agnostic contracts at the cost of serialization overhead and network latency. In practice, most teams reach for FFI only when profiling has proven that network overhead is the bottleneck — not as a first design choice.