It depends on the type of the given object you refer to as "array". If you meant the built-in type list -- yes, a copy is made:
>>> a = ['X', 'Y', 'Z']
>>> a
['X', 'Y', 'Z']
>>> b = a[0:2]
>>> b
['X', 'Y']
>>> a[0] = 42 # we modify `a`
>>> a
[42, 'Y', 'Z']
>>> b # `b` did not change
['X', 'Y']
The same applies to many other sequence types; for example, to bytearray:
>>> ar = bytearray(b'XYZ')
>>> ar
bytearray(b'XYZ')
>>> ar2 = ar[0:2]
>>> ar2
bytearray(b'XY')
>>> ar[0] = 32 # we modify `a`
>>> ar
bytearray(b' YZ')
>>> ar2 # `ar2` did not change
bytearray(b'XY')
But there are types for whom it is not the case; for example:
>>> ar = bytearray(b'XYZ')
>>> view = memoryview(ar)
>>> view
<memory at 0x...>
>>> view.tobytes()
b'XYZ'
>>> view2 = view[0:2]
>>> view2
<memory at 0x...>
>>> view2.tobytes()
b'XY'
>>> view[0] = 32 # this statement modifies both `view` and `view2`, and also `ar`!
>>> view.tobytes()
b' YZ'
>>> view2.tobytes()
b' Y'
>>> ar
bytearray(b' YZ')
Is this the same, in terms of memory usage, as using range() as in the following example?
range() (or, in Python 2, xrange()) is another story, as it does not store its items, but only the range limits (i.e., start and stop) and the optional step:
>>> range(1, 14, 2) # items 1, 3, 5, 7, 9, 11, 13 are not stored in memory
range(1, 14, 2)
>>> range(1000000000000000000000) # it does not take zillion bytes of memory :-)
range(0, 1000000000000000000000)