Edit:
To plot only the first 7 qq-plots:
fig, axes = plt.subplots(ncols=4, nrows=2, sharex=True, figsize=(4*3, 2*3))
for k, ax in zip(df.columns, np.ravel(axes)):
if k >= 7:
ax.set_visible(False)
else:
sm.qqplot(df[k], line='s', ax=ax)
ax.set_title(f'{k} QQ Plot')
plt.tight_layout()

Note: we could have limited the zip iterator to the first 7 columns by saying for k, ax in zip(df.columns[:7], np.ravel(axes)), but it would leave the 8th subplot shown (as an empty box). The method above explicitly hides the plots that we don't want.
Original answer
You can do this:
import numpy as np
fig, axes = plt.subplots(ncols=4, nrows=2, sharex=True, figsize=(4*3, 2*3))
for k, ax in zip(df.columns, np.ravel(axes)):
sm.qqplot(df[k], line='s', ax=ax)
ax.set_title(f'{k} QQ Plot')
plt.tight_layout()

Explanation
The ncols=4, nrows=2 arguments mandate 8 subplots, arranged in two rows. But the resulting axes is a nested length-2 list of length-4 lists. So we use np.ravel to flatten that. Then we use zip() to iterate concurrently on the columns of df and these axes. The iterator will stop when the shortest of columns and axes iterator is reached, which in this case is the 8 axes (so we don't even have to explicitly slice the df columns).
Finally, the plt.tight_layout() is used to space the subplots in a way that the labels are more readable and the subplots more nicely separated.