Because using str(obj) must first go through type.__call__ then str.__new__ (create a new string) then PyObject_Str (make a string out of the object) which invokes int.__str__ and, finally, uses the function you linked.
repr(obj), which corresponds to builtin_repr, directly calls PyObject_Repr (get the object repr) which then calls int.__repr__ which uses the same function as int.__str__.
Additionally, the path they take through call_function (the function that handles the CALL_FUNCTION opcode that’s generated for calls) is slightly different.
From the master branch on GitHub (CPython 3.7):
strgoes through_PyObject_FastCallKeywords(which is the one that callstype.__call__). Apart from performing more checks, this also needs to create a tuple to hold the positional arguments (see_PyStack_AsTuple).reprgoes through_PyCFunction_FastCallKeywordswhich calls_PyMethodDef_RawFastCallKeywords.repris also lucky because, since it only accepts a single argument (the switch leads it to theMETH_0case in_PyMethodDef_RawFastCallKeywords) there’s no need to create a tuple, just indexing of the args.
As your update states, this isn’t about int.__repr__ vs int.__str__, they are the same function after all; it’s all about how repr and str reach them. str just needs to work a bit harder.