Type recast on assignment

I did recently stumble upon strange error what made me rethink all about Python type assignment. The code in question declared loop iteration variable and then incremented it with counter+=valuearray_uint8[index] It turned out that for Miniforge aarch64 such statement did change left side type to uint8 while LTS 20.04 Python from apt behaved ok.
This brings on interesting question for reassignment. Knowing that all Python values derive from PyObject common class is most certainly possible to reassign types. The main question is when was such behaviour decided? For sure if you assign incompatible types then either recast or exception are two ways to handle issue and it should be selectable with compiler argument e.g. --strict. For numbers where type conversion is possible for example moving between int lengths interpreter should just convert result to corresponding left size type before assignment but never change left hand type.


Do you have a code example for reproducing this? Ideally with a link to mypy/pyright playgrounds. Also, which type checker were you using? Is it possible the difference you saw was because of missing stubs and hence defaulting to Any in one of the versions?

Included is test script i used and also printout that shows same results for

  1. IMX8 aarch64 24.04 LTS and deadsnake Python 10
  2. MACOS miniconda and native Python
    It seems to consistently get results from result of calculation and then reassign types when needed. Also it came to me that numpy and ctypes are not python native and if something is strange then these originate from respective packages. For native case when one argument is float then result is recast to float.
    Of course such behaviour can be overridden with explicit type conversions but given that there is no memory or speed gain in using smaller data values then it is safer to stick to native python types. It can also slip under radar if loop exit result fits in small type, for uint8_t case is smaller than 255.

Script run results (script is included):

(flexlink) Dalos/realgit/dalos/vspaotse $ python recast.py 
Native result type after declaration <class 'int'> <class 'int'>
au8 and bu8 types after declaration <class 'numpy.uint8'> <class 'numpy.uint8'>
/home/prj/Dalos/realgit/dalos/vspaotse/recast.py:13: RuntimeWarning: overflow encountered in scalar add
  X1 += au8np
Increment type <class 'numpy.uint8'>
Assign type <class 'numpy.uint8'>
/home/prj/Dalos/realgit/dalos/vspaotse/recast.py:17: RuntimeWarning: overflow encountered in scalar add
  X3 = au8np  + bu8np
Addition type <class 'numpy.uint8'>
/home/prj/Dalos/realgit/dalos/vspaotse/recast.py:19: RuntimeWarning: overflow encountered in scalar add
  X4 = au8np  + 100
Addition ofconstant and uint8 type <class 'numpy.uint8'>
au8 and bu8 types after declaration <class 'ctypes.c_ubyte'> <class 'ctypes.c_ubyte'>
Increment result type <class 'int'>
Assign result type <class 'int'>
Addition result type <class 'int'>
Addition of constant and uint8 result type <class 'int'>
afpy and bipy types after declaration <class 'float'> <class 'int'>
Increment type <class 'float'>
Assign type <class 'float'>
Addition type <class 'float'>
Addition of constant and int type <class 'float'>

I did not find include link so here is the recast.py script

# check variable type recast during assignment
import numpy as np
import ctypes as ct
X1 = int(100)
X2 = int(0)
X3 = int(0)
X4 = int(0)
print("Native result type after declaration %s %s" % (type(X1), type(X2)))
au8np = np.uint8(200)
bu8np = np.uint8(100)
print ("NUMPY")
print("au8 and bu8 types after declaration %s %s" % (type(au8np), type(bu8np)))
X1 += au8np
print("Increment type %s" % type(X1))
X2 = au8np
print("Assign type %s" % type(X2))
X3 = au8np + bu8np
print("Addition type %s" % type(X3))
X4 = au8np + 100
print("Addition ofconstant and uint8 type %s" % type(X4))
au8ct = ct.c_uint8(200)
bu8ct = ct.c_uint8(100)
X1 = int(100)
X2 = int(0)
X3 = int(0)
X4 = int(0)
print("au8 and bu8 types after declaration %s %s" % (type(au8ct), type(bu8ct)))
X1 += au8ct.value # ctypes must be converted to int
print("Increment result type %s" % type(X1))
X2 = au8ct.value
print("Assign result type %s" % (type(X2)))
X3 = au8ct.value + bu8ct.value
print("Addition result type %s" % type(X3))
X4 = au8ct.value + 100
print("Addition of constant and uint8 result type %s" % type(X4))
afpy = float(200.0)
bipy = int(100)
X1 = int(100)
X2 = int(200)
X3 = int(0)
X4 = int(0)
print("afpy and bipy types after declaration %s %s" % (type(afpy), type(bipy)))
X1 += afpy
print("Increment type %s" % type(X1))
X2 = afpy
print("Assign type %s" % type(X2))
X3 = afpy + bipy
print("Addition type %s" % type(X3))
X4 = afpy + 100
print("Addition of constant and int type %s" % type(X4))

For numpy there seems to be nice solution how to modify existing code with minimal changes. Although variable does not have value attribute as ctypes it has tolist() what does the same - returns value as single integer. We can just add it to where variable in uint8 format is used and all should be ok.

Thanks for response. Here is where i lack knowledge and do not know even what stubs mean. I come from Verilog / VHDL / C / C++ background where calculations never change declared variable type. I know it can be done in Python for exampleyou can start frominteger and reassign to string.
Myself have never used such smaller types because these provide no benefit except for ctypes/structure when accessing shared mapped memory from /dev/mem through pointer from Python c module.
Now there is task where system architect writes code in Python and I have to take it to NXP LA9310 VSPA dsp. Here is the first time i noticed it when model ends up in infinite loop because of loop counter overflow.