print() function is a ream of code, under the hood. The net effect is to stream character data to the standard output, in this case the console.
str() creates a string representation in much the same way as
print() creates the stream, which means overlapping purposes.
The only need for casting to
str is if we are concatenating with another
str or carrying out a
print() is a built-in that accepts all types.
That last sentence kind of over generalizes and suggests that print will interpret every argument, which is not the case at all. My bad.
>>> print (range(10))
>>> print (map(lambda x: x ** 2, range(10)))
<map object at 0x0000028E8E30EFD0>
>>> print (filter(lambda x: x % 2, range(10)))
<filter object at 0x0000028E8E30EFD0>
>>> print (enumerate(range(10)))
<enumerate object at 0x0000028E8E2D4640>
The only thing printed was the stream of the slug descriptor. There is an iterator sitting and waiting at some address is all that this is telling us. So, yes, we do need to recast some types to a readable or iterable type to get them to reveal their contents. Iterators that give up their contents are consumed so printing them will destroy them (empty them of their references).
range() is an exception to this rule as it is not a true iterator.