The difference is between basic indexing and advanced. Indexing with slices and scalars is 'basic'. When one or more of the indexes are lists or array, it uses 'advanced'.
Docs on advanced v basic indexing
There are various ways of producing the kind of indexing that you want. np.ix_ is designed to make this easy:
In [80]: np.ix_([0,1], [0], [3,4], [0,1])
Out[80]:
(array([[[[0]]],
[[[1]]]]), array([[[[0]]]]), array([[[[3],
[4]]]]), array([[[[0, 1]]]]))
In [79]: arr[np.ix_([0,1], [0], [3,4], [0,1])].shape
Out[79]: (2, 1, 2, 2)
To pass all indices through ix_ I had to make the 2nd one a list. A way around that is to use ix_ on just 3 lists, and add that 0 later:
In [81]: I,J,K=np.ix_([0,1], [3,4], [0,1])
In [82]: arr[I,i,J,K].shape
Out[82]: (2, 2, 2)
In [83]: I,J,K
Out[83]:
(array([[[0]],
[[1]]]), array([[[3],
[4]]]), array([[[0, 1]]]))
Note the shape of the index arrays:
In [84]: I.shape
Out[84]: (2, 1, 1)
In [85]: J.shape
Out[85]: (1, 2, 1)
In [86]: K.shape
Out[86]: (1, 1, 2)
together they broadcast to a (2,2,2) shape. So any index arrays that broadcast that way should work. You could make J with
In [87]: J
Out[87]:
array([[[3],
[4]]])
In [88]: np.array([3,4])[None,:,None]
Out[88]:
array([[[3],
[4]]])