IO functions like putStrLn is curried by default, so you can pass them around without executing them. But this does not affect the purity of executing them. And it is not something special for Haskell, you can pass functions around without executing them in almost any language.
The cool thing about Haskell is that code is pure by default, and it is only due to special-casing by the language (the type of "main" being "IO ()") that you can even have operations with side-effects.
Haskell allows you to have parts of the program guaranteed pure, but the program is a whole is not pure if it does any input/output.