diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,3 +1,9 @@
+## 1.2.0
+
+* Don't generalize I/O functions to `IOData`, instead specialize to
+  `ByteString`. See:
+  http://www.snoyman.com/blog/2016/12/beware-of-readfile#real-world-failures
+
 ## 1.0.2
 
 * Export `parseTimeM` for `time >= 1.5`
diff --git a/ClassyPrelude.hs b/ClassyPrelude.hs
--- a/ClassyPrelude.hs
+++ b/ClassyPrelude.hs
@@ -125,7 +125,13 @@
     , charToLower
     , charToUpper
       -- ** IO
-    , IOData (..)
+    , readFile
+    , readFileUtf8
+    , writeFile
+    , writeFileUtf8
+    , hGetContents
+    , hPut
+    , hGetChunk
     , print
     , hClose
       -- ** Difference lists
@@ -172,7 +178,6 @@
 import Data.Mutable
 import Data.Traversable (Traversable (..), for, forM)
 import Data.Foldable (Foldable)
-import Data.IOData (IOData (..))
 import Control.Monad.Base
 import Control.Monad.Trans.Unlift
 import qualified Control.Concurrent.Async as Async
@@ -208,7 +213,9 @@
 import Data.Containers
 import Data.Builder
 import Data.NonNull
+import qualified Data.ByteString
 import Data.ByteString.Internal (ByteString (PS))
+import Data.ByteString.Lazy.Internal (defaultChunkSize)
 import Data.Vector.Storable (unsafeToForeignPtr, unsafeFromForeignPtr)
 
 import System.IO (Handle, stdin, stdout, stderr, hClose)
@@ -567,3 +574,52 @@
 -- @since 1.0.0
 link2Async :: MonadIO m => Async a -> Async b -> m ()
 link2Async a = liftIO . Async.link2 a
+
+-- | Strictly read a file into a 'ByteString'.
+--
+-- @since 1.2.0
+readFile :: MonadIO m => FilePath -> m ByteString
+readFile = liftIO . Data.ByteString.readFile
+
+-- | Strictly read a file into a 'Text' using a UTF-8 character
+-- encoding. In the event of a character encoding error, a Unicode
+-- replacement character will be used (a.k.a., @lenientDecode@).
+--
+-- @since 1.2.0
+readFileUtf8 :: MonadIO m => FilePath -> m Text
+readFileUtf8 = liftM decodeUtf8 . readFile
+
+-- | Write a 'ByteString' to a file.
+--
+-- @since 1.2.0
+writeFile :: MonadIO m => FilePath -> ByteString -> m ()
+writeFile fp = liftIO . Data.ByteString.writeFile fp
+
+-- | Write a 'Text' to a file using a UTF-8 character encoding.
+--
+-- @since 1.2.0
+writeFileUtf8 :: MonadIO m => FilePath -> Text -> m ()
+writeFileUtf8 fp = writeFile fp . encodeUtf8
+
+-- | Strictly read the contents of the given 'Handle' into a
+-- 'ByteString'.
+--
+-- @since 1.2.0
+hGetContents :: MonadIO m => Handle -> m ByteString
+hGetContents = liftIO . Data.ByteString.hGetContents
+
+-- | Write a 'ByteString' to the given 'Handle'.
+--
+-- @since 1.2.0
+hPut :: MonadIO m => Handle -> ByteString -> m ()
+hPut h = liftIO . Data.ByteString.hPut h
+
+-- | Read a single chunk of data as a 'ByteString' from the given
+-- 'Handle'.
+--
+-- Under the surface, this uses 'Data.ByteString.hGetSome' with the
+-- default chunk size.
+--
+-- @since 1.2.0
+hGetChunk :: MonadIO m => Handle -> m ByteString
+hGetChunk = liftIO . flip Data.ByteString.hGetSome defaultChunkSize
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@
 ====================
 
 * use the NoImplicitPrelude extension (you can place this in your cabal file) and `import ClassyPrelude`
-* use [base-noprelude](https://github.com/hvr/base-noprelude) in your project and define a Prelude module that re-exports `ClassyPrelue`.
+* use [base-noprelude](https://github.com/hvr/base-noprelude) in your project and define a Prelude module that re-exports `ClassyPrelude`.
 
 
 Appendix
diff --git a/classy-prelude.cabal b/classy-prelude.cabal
--- a/classy-prelude.cabal
+++ b/classy-prelude.cabal
@@ -1,5 +1,5 @@
 name:                classy-prelude
-version:             1.0.2
+version:             1.2.0
 synopsis:            A typeclass-based Prelude.
 description:         Modern best practices without name collisions. No partial functions are exposed, but modern data structures are, without requiring import lists. Qualified modules also are not needed: instead operations are based on type-classes from the mono-traversable package.
 
