This is because of lost of precision. It turns out, doing matrix exponential needs too many terms to converge (around 35 in this case) and the matrix M^35 already exploded your integer. Using the same algorithm, let's see how Julia can do it:
julia> M = [-5 2 3; 2 -6 4; 4 5 -9]
3×3 Array{Int64,2}:
-5 2 3
2 -6 4
4 5 -9
julia> exp(M)
3×3 Array{Float64,2}:
0.365957 0.354538 0.279505
0.365275 0.3551 0.279625
0.365515 0.354899 0.279585
julia> term = (n) -> (M^n)/factorial(big(n))
#1 (generic function with 1 method)
julia> sum(term, 0:40)
3×3 Array{BigFloat,2}:
282.793 439.914 2167.11
548.843 -1876.45 1328.97
1753.07 3838.5 -5590.57
julia> M^20
3×3 Array{Int64,2}:
8757855768227185495 5428672870161680643 4260215435320685478
2846510725988846806 -6309877790968251876 3463367064979405070
1252813985306285990 3038127759137839419 -4290941744444125409
julia> M = Matrix{Int128}(M)
3×3 Array{Int128,2}:
-5 2 3
2 -6 4
4 5 -9
julia> M^20
3×3 Array{Int128,2}:
691287386495480595287 1259807269882411190531 -1951094656377891785818
1423245804401624321238 2594681036602078525980 -4017926841003702847218
-2710418564849997801562 -4940689283995021993669 7651107848845019795231
julia> sum(term, 0:40)
3×3 Array{BigFloat,2}:
0.365246 0.353079 0.28076
0.363873 0.353114 0.283013
0.367464 0.358922 0.305631
From the above, you see the big difference for M^20
when the matrix is in Int64 vs Int128. Indeed, it silently overflow the integer without throwing exceptions. That's the same reason you got the answer wrong if you sum the terms up.
Unfortunately, numpy does not have int128 type as Julia. But we do have float128. So let's modify your code to use float128 instead:
>>> from scipy.linalg import expm
>>> import numpy as np
>>> import math
>>> M = np.array([[-5, 2, 3], [2, -6, 4], [4, 5, -9]])
>>> M
array([[-5, 2, 3],
[ 2, -6, 4],
[ 4, 5, -9]])
>>> expm(M)
array([[0.3659571 , 0.35453832, 0.27950458],
[0.36527461, 0.35510049, 0.27962489],
[0.36551524, 0.35489926, 0.27958549]])
>>> np.linalg.matrix_power(M, 20)
array([[ 8757855768227185495, 5428672870161680643, 4260215435320685478],
[ 2846510725988846806, -6309877790968251876, 3463367064979405070],
[ 1252813985306285990, 3038127759137839419, -4290941744444125409]])
>>> term = lambda n: np.linalg.matrix_power(M, n)/float(math.factorial(n))
>>> sum([term(n) for n in range(40)])
array([[ 282.79272291, 439.91385783, 2167.11075278],
[ 548.84303052, -1876.45101128, 1328.96835279],
[ 1753.07193608, 3838.50198385, -5590.57463349]])
>>> M = M.astype('float128')
>>> M
array([[-5., 2., 3.],
[ 2., -6., 4.],
[ 4., 5., -9.]], dtype=float128)
>>> np.linalg.matrix_power(M, 20)
array([[ 6.91287386e+20, 1.25980727e+21, -1.95109466e+21],
[ 1.42324580e+21, 2.59468104e+21, -4.01792684e+21],
[-2.71041856e+21, -4.94068928e+21, 7.65110785e+21]],
dtype=float128)
>>> sum([term(n) for n in range(40)])
array([[0.36595003, 0.35452543, 0.27952454],
[0.36526005, 0.35507395, 0.279666 ],
[0.36554297, 0.35494981, 0.27950722]], dtype=float128)
Same here, you see the difference of matrix M
raise to 20th power when the data type are different. You lost some precision by using float, but you don't overflow to early and get your answer right, at least for this particular matrix.
Lesson learned: Do not implement your own function if scipy provide one for you. There is a reason people implement it in a library.