Troubleshooting¶
This section provides some general troubleshooting advice about commonly-seen errors. If you’re having a problem with Cython it may be worth reading this. If you encounter a commonly-seen error that we haven’t covered here, we’d appreciate a PR adding it to this section!
Where the language is “messy”¶
By necessity, Cython is a slightly odd mix of the resolved-at-run-time, dynamic behaviour of Python, and the statically-defined, resolved at compile-time behaviour of C. These don’t always combine perfectly, and the places that often cause confusion are often the places they meet.
As example, for a cdef class
, Cython is able to access cdef
attributes
directly (as a simple C lookup). However, if the direct attribute lookup “misses”
then Cython doesn’t produce an error message - instead it assumes that it will
be able to resolve that attribute through the standard Python “string lookup from
a dictionary” mechanism at runtime. The two mechanisms are quite different in
how they work and what they can return (the Python mechanism can only return
Python objects, while the the direct lookup can return largely any C type).
Much the same can occur when a name is imported rather “cimported” - Cython does not know where the name comes from so treats it as a regular Python object.
This silent-fallback to Python behaviour is often a source of confusion. In the
best case it gives the same overall behaviour but slightly slower (for example
calling a cpdef
function through the Python mechanism rather than directly
to C). Often it just causes an AttributeError
exception at runtime. Very
occasionally it might do something quite different - invoke a Python method
with the same name as a cdef
method, or cause a convert from a C++ container
to a Python one.
This kind of dual-layered behaviour probably isn’t how one would design a language from scratch, but is needed for Cython’s goals for being Python compatible and allowing C types to be used fairly seamlessly.
AttributeErrors
¶
Untyped objects¶
A common reason to get AttributeErrors
is that Cython does not know the type of your
object:
cdef class Counter:
cdef int count_so_far
...
The attribute count_so_far
is only accessible from Cython code, and Cython accesses
it through a direct lookup into the C struct that it defines for Counter
(i.e.
it’s really quick!).
Now try run the following Cython code on a pair of Counter
objects:
def bigger_count(c1, c2):
return c1.count_so_far < c2.count_so_far
This will give an AttributeError
because Cython does not know the types of c1
and c2
. Typing them as Counter c1
and Counter c2
fixes the problem:
def bigger_count(c1, c2):
return c1.count_so_far < c2.count_so_far
A common variation of the same problem happens for global objects:
def count_something():
c = Counter()
# code goes here!!!
print(c.count_so_far) # works
global_count = Counter()
print(global_count.count_so_far) # AttributeError!
Within a function Cython usually manages to infer the type. So it knows that c
is a Counter
even though you have not told it. However the same doesn’t apply at global/module scope. Here
there’s a strong assumption that you want objects to be exposed as Python attributes of the
module (and remember that Python attributes could be modified from elsewhere…), so Cython
essentially disables all type-inference. Therefore it doesn’t know the type of global_count
.
Writing into extension types¶
AttributeErrors
can also happen when writing into a cdef class
, commonly in __init__
:
cdef class Company:
def __init__(self, staff):
self.staff = staff # AttributeError!
Unlike a regular class, cdef class
has a fixed list of attributes that you can write to and
you need to declare them explicitly. For example:
cdef class Company:
cdef list staff
# ...
(use cdef staff
or cdef object staff
if you don’t want to specify a type). If you do want
the ability to add arbitrary attributes then you can add a __dict__
member:
cdef class Company:
cdef dict __dict__
def __init__(self, staff):
self.staff = staff
This gives extra flexibility, but loses some of the performance benefits of using an extension type. It also adds restrictions to inheritance.
Extension type class attributes vs instance attributes¶
A common pattern in Python (used a lot within the Cython code-base itself) is to use instance attributes that shadow class attributes:
class Email:
message = "hello" # sensible default
def actually_I_really_dislike_this_person(self):
self.message = "go away!"
On access to message
Python first looks up the instance dictionary to see if it
has a value for message
and if that fails looks up the class dictionary to get
the default value. The advantages are
it provides an easy sensible default,
it potentially saves a bit of memory by not populating the instance dictionary if not necessary (although modern versions of Python are pretty good at sharing keys for common attributes between instances),
it saves a bit of time reference counting (vs if you initialized the defaults in the constructor),
Cython extension types don’t support this pattern. You should just set the
defaults in the constructor. If you don’t set defaults for a cdef
attribute then
they’ll be set to an “empty” value (None
for Python object attributes).
Pitfalls of automatic type conversions¶
Cython automatically generates type conversions between certain C/C++ types and Python types. These are often undesirable.
First we should look at what conversions Cython generates:
C
struct
to/from Pythondict
- if all elements of astruct
are themselves convertible to a Python object, then thestruct
will be converted to a Pythondict
if returned from a function that returns a Python object:# taken from the Cython documentation cdef struct Grail: int age float volume def get_grail(): cdef Grail g g.age = 100 g.volume = 2.5 return g print(get_grail()) # prints something similar to: # {'age': 100, 'volume': 2.5}
C++ standard library containers to/from their Python equivalent. A common pattern is to use a
def
function with an argument typed asstd::vector
. This will be auto-converted from a Python list:from libcpp.vector cimport vector def print_list(vector[int] x): for xi in x: print(x)
Most of these conversions should work both ways.
They have a couple of non-obvious downsides.
The conversion isn’t free¶
Especially for the C++ container conversions. Consider the print_list
function above. The
function is appealing because iteration over the vector is faster than iteration over a Python
list. However, Cython must iterate over each element of your input list, checking that it is
something convertible to a C integer. Therefore, you haven’t actually saved yourself any time -
you’ve just hidden the “expensive” loop in a function signature.
These conversions may be worthwhile if you’re doing sufficient work inside your function. You should also consider also having a single place in your Cython code where the conversion happens as your interface to Python, then keeping the type as the C++ type and working on it across multiple Cython functions.
In many cases it might be better to type your function with a 1D typed memoryview (int[:]
)
and pass in an array.array
or a Numpy array instead of using a C++ vector.
Changes do not propagate back¶
Especially to attributes of cdef classes
exposed to Python via properties (including
via cdef public
attributes).
For example:
from libcpp.vector cimport vector
cdef class VecHolder:
def __init__(self, max):
self.value = list(range(max)) # just fill it for demo purposes
cdef public vector[double] values
then from Python:
vh = VecHolder(5)
print(vh.values)
# Output: [ 0, 1, 2, 3, 4 ]
vh.values[0] = 100
print(vh.values)
# Output: [ 0, 1, 2, 3, 4 ]
# However you can re-assign it completely
vh.values = []
print(vh.values)
# Output: []
Essentially your Python code modifies the list
that is returned to it an not the underlying
vector
used to generate the list
. This is sufficiently non-intuitive that I really
recommend against exposing convertible types as attributes!