diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## v0.2.1
+
+* Add `P.list`
+
 ## v0.2.0
 
 * Move setting properties to `Skeletest.Prop`
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -219,7 +219,7 @@
 * `P.returns (P.gt 10)`
     * Satisfied when the left hand side is an `IO` action that returns a value greater than `10`.
 
-* `P.throws MyException`
+* `P.throws (P.eq MyException)`
     * Satisfied when the left hand side is an `IO` action that throws the given exception.
 
 ### Unit and Integration tests
diff --git a/skeletest.cabal b/skeletest.cabal
--- a/skeletest.cabal
+++ b/skeletest.cabal
@@ -1,7 +1,7 @@
 cabal-version: 3.0
 
 name: skeletest
-version: 0.2.0
+version: 0.2.1
 synopsis: Batteries-included, opinionated test framework
 description: Batteries-included, opinionated test framework. See README.md for more details.
 homepage: https://github.com/brandonchinn178/skeletest#readme
diff --git a/src/Skeletest/Assertions.hs b/src/Skeletest/Assertions.hs
--- a/src/Skeletest/Assertions.hs
+++ b/src/Skeletest/Assertions.hs
@@ -45,12 +45,17 @@
 
 infix 1 `shouldBe`, `shouldNotBe`, `shouldSatisfy`, `shouldNotSatisfy`
 
+-- | Assert that the given input should match the given value.
+-- Equivalent to @actual `shouldSatisfy` P.eq expected@
 shouldBe :: (HasCallStack, Testable m, Eq a) => a -> a -> m ()
 actual `shouldBe` expected = GHC.withFrozenCallStack $ actual `shouldSatisfy` P.eq expected
 
+-- | Assert that the given input should not match the given value.
+-- Equivalent to @actual `shouldNotSatisfy` P.eq expected@
 shouldNotBe :: (HasCallStack, Testable m, Eq a) => a -> a -> m ()
 actual `shouldNotBe` expected = GHC.withFrozenCallStack $ actual `shouldNotSatisfy` P.eq expected
 
+-- | Assert that the given input should satisfy the given predicate.
 shouldSatisfy :: (HasCallStack, Testable m) => a -> Predicate m a -> m ()
 actual `shouldSatisfy` p =
   GHC.withFrozenCallStack $
@@ -58,6 +63,7 @@
       PredicateSuccess -> pure ()
       PredicateFail msg -> failTest' msg
 
+-- | Assert that the given input should not satisfy the given predicate.
 shouldNotSatisfy :: (HasCallStack, Testable m) => a -> Predicate m a -> m ()
 actual `shouldNotSatisfy` p = GHC.withFrozenCallStack $ actual `shouldSatisfy` P.not p
 
@@ -67,6 +73,7 @@
     (modifyIORef failContextRef (Text.pack msg :))
     (modifyIORef failContextRef (drop 1))
 
+-- | Unconditionally fail the test with the given message.
 failTest :: (HasCallStack, Testable m) => String -> m a
 failTest = GHC.withFrozenCallStack $ failTest' . Text.pack
 
diff --git a/src/Skeletest/Internal/Predicate.hs b/src/Skeletest/Internal/Predicate.hs
--- a/src/Skeletest/Internal/Predicate.hs
+++ b/src/Skeletest/Internal/Predicate.hs
@@ -27,6 +27,7 @@
   nothing,
   left,
   right,
+  list,
   IsPredTuple (..),
   tup,
   con,
@@ -203,6 +204,7 @@
 
 {----- General -----}
 
+-- | A predicate that matches any value
 anything :: (Monad m) => Predicate m a
 anything =
   Predicate
@@ -219,23 +221,41 @@
 
 {----- Ord -----}
 
+-- | A predicate checking if the input is equal to the given value
+--
+-- >>> 1 `shouldSatisfy` P.eq 1
 eq :: (Eq a, Monad m) => a -> Predicate m a
 eq = mkPredicateOp "=" "≠" $ \actual expected -> actual == expected
 
+-- | A predicate checking if the input is greater than the given value
+--
+-- >>> 1 `shouldSatisfy` P.gt 0
 gt :: (Ord a, Monad m) => a -> Predicate m a
 gt = mkPredicateOp ">" "≯" $ \actual expected -> actual > expected
 
+-- | A predicate checking if the input is greater than or equal to the given value
+--
+-- >>> 1 `shouldSatisfy` P.gte 0
 gte :: (Ord a, Monad m) => a -> Predicate m a
 gte = mkPredicateOp "≥" "≱" $ \actual expected -> actual > expected Prelude.|| actual == expected
 
+-- | A predicate checking if the input is less than the given value
+--
+-- >>> 1 `shouldSatisfy` P.lt 10
 lt :: (Ord a, Monad m) => a -> Predicate m a
 lt = mkPredicateOp "<" "≮" $ \actual expected -> actual < expected
 
+-- | A predicate checking if the input is less than or equal to the given value
+--
+-- >>> 1 `shouldSatisfy` P.lte 10
 lte :: (Ord a, Monad m) => a -> Predicate m a
 lte = mkPredicateOp "≤" "≰" $ \actual expected -> actual < expected Prelude.|| actual == expected
 
 {----- Data types -----}
 
+-- | A predicate checking if the input is Just, wrapping a value matching the given predicate.
+--
+-- >>> Just 1 `shouldSatisfy` P.just (P.gt 0)
 just :: (Monad m) => Predicate m a -> Predicate m (Maybe a)
 just p = conMatches "Just" fieldNames toFields preds
   where
@@ -245,6 +265,9 @@
       _ -> Nothing
     preds = HCons p HNil
 
+-- | A predicate checking if the input is Nothing
+--
+-- >>> Nothing `shouldSatisfy` P.nothing
 nothing :: (Monad m) => Predicate m (Maybe a)
 nothing = conMatches "Nothing" fieldNames toFields preds
   where
@@ -254,6 +277,9 @@
       _ -> Nothing
     preds = HNil
 
+-- | A predicate checking if the input is Left, wrapping a value matching the given predicate.
+--
+-- >>> Left 1 `shouldSatisfy` P.left (P.gt 0)
 left :: (Monad m) => Predicate m a -> Predicate m (Either a b)
 left p = conMatches "Left" fieldNames toFields preds
   where
@@ -263,6 +289,9 @@
       _ -> Nothing
     preds = HCons p HNil
 
+-- | A predicate checking if the input is Right, wrapping a value matching the given predicate.
+--
+-- >>> Right 1 `shouldSatisfy` P.right (P.gt 0)
 right :: (Monad m) => Predicate m b -> Predicate m (Either a b)
 right p = conMatches "Right" fieldNames toFields preds
   where
@@ -272,6 +301,34 @@
       _ -> Nothing
     preds = HCons p HNil
 
+-- | A predicate checking if the input is a list matching exactly the given predicates.
+--
+-- >>> [1, 2, 3] `shouldSatisfy` P.list [P.eq 1, P.eq 2, P.eq 3]
+-- >>> [1, 2, 3] `shouldNotSatisfy` P.list [P.eq 1, P.eq 2]
+-- >>> [1, 2, 3] `shouldNotSatisfy` P.list [P.eq 1, P.eq 2, P.eq 3, P.eq 4]
+--
+-- @since 0.2.1
+list :: (Monad m) => [Predicate m a] -> Predicate m [a]
+list predList =
+  Predicate
+    { predicateFunc = \actual ->
+        if length actual == length predList
+          then verifyAll listify <$> sequence (zipWith predicateFunc predList actual)
+          else
+            pure
+              PredicateFuncResult
+                { predicateSuccess = False
+                , predicateExplain = "Got different number of elements"
+                , predicateShowFailCtx = ShowFailCtx
+                }
+    , predicateDisp = disp
+    , predicateDispNeg = dispNeg
+    }
+  where
+    listify vals = "[" <> Text.intercalate ", " vals <> "]"
+    disp = listify $ map predicateDisp predList
+    dispNeg = "not " <> disp
+
 class IsTuple a where
   type TupleArgs a :: [Type]
   toHList :: a -> HList Identity (TupleArgs a)
@@ -310,11 +367,10 @@
   type ToPredTuple m (a, b, c, d, e, f) = (Predicate m a, Predicate m b, Predicate m c, Predicate m d, Predicate m e, Predicate m f)
   toHListPred _ (a, b, c, d, e, f) = HCons a . HCons b . HCons c . HCons d . HCons e . HCons f $ HNil
 
--- | Matches a tuple satisfying the given predicates. Works for tuples up to 6 elements.
+-- | A predicate checking if the input matches a tuple matching the given predicates.
+-- Works for tuples up to 6 elements.
 --
--- @
--- P.tup (P.eq 1, P.gt 2, P.hasPrefix "hello ")
--- @
+-- >>> (1, 10, "hello world") `shouldSatisfy` P.tup (P.eq 1, P.gt 2, P.hasPrefix "hello ")
 tup :: forall a m. (IsPredTuple m a, Monad m) => ToPredTuple m a -> Predicate m a
 tup predTup =
   Predicate
@@ -329,13 +385,13 @@
     disp = tupify $ HList.toListWith predicateDisp preds
     dispNeg = "not " <> disp
 
--- | A predicate for checking that a value matches the given constructor.
+-- | A predicate checking if the input matches the given constructor.
 --
 -- It takes one argument, which is the constructor, except with all fields
 -- taking a Predicate instead of the normal value. Skeletest will rewrite
 -- the expression so it typechecks correctly.
 --
--- >>> user `shouldSatisfy` P.con User{name = P.eq "user1", email = P.contains "@"}
+-- >>> User "user1" "user1@example.com" `shouldSatisfy` P.con User{name = P.eq "user1", email = P.contains "@"}
 --
 -- Record fields that are omitted are not checked at all; i.e.
 -- @P.con Foo{}@ and @P.con Foo{a = P.anything}@ are equivalent.
@@ -386,7 +442,7 @@
 
 {----- Numeric -----}
 
--- | A predicate for checking that a value is equal within some tolerance.
+-- | A predicate checking if the input is equal to the given value within some tolerance.
 --
 -- Useful for checking equality with floats, which might not be exactly equal.
 -- For more information, see: https://jvns.ca/blog/2023/01/13/examples-of-floating-point-problems/.
@@ -413,11 +469,18 @@
       | x < 0 = error $ "tolerance can't be negative: " <> show x
       | otherwise = fromRational x
 
+-- | The tolerance to use in 'approx'.
+--
+-- An input satisfies a tolerance if it's within the relative tolerance
+-- (rel * input) or the absolute tolerance of the reference value.
 data Tolerance = Tolerance
   { rel :: Maybe Rational
+  -- ^ If provided, the relative tolerance. Defaults to 1e-6.
   , abs :: Rational
+  -- ^ The absolute tolerance. Defaults to 1e-12.
   }
 
+-- | The default tolerance for 'approx'.
 tol :: Tolerance
 tol = Tolerance{rel = Just 1e-6, abs = 1e-12}
 
@@ -425,6 +488,9 @@
 
 infixr 1 <<<, >>>
 
+-- | A predicate checking if the input matches the given predicate, after applying the given function.
+--
+-- >>> "hello" `shouldSatisfy` (P.eq 5 P.<<< length)
 (<<<) :: (Monad m) => Predicate m a -> (b -> a) -> Predicate m b
 Predicate{..} <<< f =
   Predicate
@@ -433,9 +499,15 @@
     , predicateDispNeg
     }
 
+-- | Same as '<<<', except with the arguments flipped.
+--
+-- >>> "hello" `shouldSatisfy` (length P.>>> P.eq 5)
 (>>>) :: (Monad m) => (b -> a) -> Predicate m a -> Predicate m b
 (>>>) = flip (<<<)
 
+-- | A predicate checking if the input does not match the given predicate
+--
+-- >>> Just 2 `shouldSatisfy` P.just (P.not (P.eq 1))
 not :: (Monad m) => Predicate m a -> Predicate m a
 not Predicate{..} =
   Predicate
@@ -446,12 +518,21 @@
     , predicateDispNeg = predicateDisp
     }
 
+-- | A predicate checking if the input matches both of the given predicates
+--
+-- >>> 1 `shouldSatisfy` P.gt 0 P.&& P.lt 2
 (&&) :: (Monad m) => Predicate m a -> Predicate m a -> Predicate m a
 p1 && p2 = and [p1, p2]
 
+-- | A predicate checking if the input matches one of the given predicates
+--
+-- >>> 1 `shouldSatisfy` P.lt 5 P.|| P.gt 10
 (||) :: (Monad m) => Predicate m a -> Predicate m a -> Predicate m a
 p1 || p2 = or [p1, p2]
 
+-- | A predicate checking if the input matches all of the given predicates
+--
+-- >>> 1 `shouldSatisfy` P.and [P.gt 0, P.lt 2]
 and :: (Monad m) => [Predicate m a] -> Predicate m a
 and preds =
   Predicate
@@ -464,6 +545,9 @@
     andify = Text.intercalate "\nand "
     predList = map (parens . predicateDisp) preds
 
+-- | A predicate checking if the input matches any of the given predicates
+--
+-- >>> 1 `shouldSatisfy` P.or [P.lt 5, P.gt 10]
 or :: (Monad m) => [Predicate m a] -> Predicate m a
 or preds =
   Predicate
@@ -478,6 +562,9 @@
 
 {----- Containers -----}
 
+-- | A predicate checking if the input is a list-like type where some element matches the given predicate.
+--
+-- >>> [1, 2, 3] `shouldSatisfy` P.any (P.eq 1)
 any :: (Foldable t, Monad m) => Predicate m a -> Predicate m (t a)
 any Predicate{..} =
   Predicate
@@ -487,6 +574,9 @@
     , predicateDispNeg = "no elements matching " <> parens predicateDisp
     }
 
+-- | A predicate checking if the input is a list-like type where all elements match the given predicate.
+--
+-- >>> [1, 2, 3] `shouldSatisfy` P.all (P.gt 0)
 all :: (Foldable t, Monad m) => Predicate m a -> Predicate m (t a)
 all Predicate{..} =
   Predicate
@@ -496,6 +586,10 @@
     , predicateDispNeg = "some elements not matching " <> parens predicateDisp
     }
 
+-- | A predicate checking if the input is a list-like type where the given element is present.
+-- Equivalent to @P.any . P.eq@.
+--
+-- >>> [1, 2, 3] `shouldSatisfy` P.elem 1
 elem :: (Eq a, Foldable t, Monad m) => a -> Predicate m (t a)
 elem = any . eq
 
@@ -514,6 +608,10 @@
   isInfixOf = Text.isInfixOf
   isSuffixOf = Text.isSuffixOf
 
+-- | A predicate checking if the input has the given prefix
+--
+-- >>> [1, 2, 3] `shouldSatisfy` P.hasPrefix [1, 2]
+-- >>> "hello world" `shouldSatisfy` P.hasPrefix "hello "
 hasPrefix :: (HasSubsequences a, Monad m) => a -> Predicate m a
 hasPrefix prefix =
   Predicate
@@ -535,6 +633,10 @@
     disp = "has prefix " <> render prefix
     dispNeg = "does not have prefix " <> render prefix
 
+-- | A predicate checking if the input contains the given subsequence
+--
+-- >>> [1, 2, 3] `shouldSatisfy` P.hasInfix [2]
+-- >>> "hello world" `shouldSatisfy` P.hasInfix "ello"
 hasInfix :: (HasSubsequences a, Monad m) => a -> Predicate m a
 hasInfix elems =
   Predicate
@@ -556,6 +658,10 @@
     disp = "has infix " <> render elems
     dispNeg = "does not have infix " <> render elems
 
+-- | A predicate checking if the input has the given suffix
+--
+-- >>> [1, 2, 3] `shouldSatisfy` P.hasSuffix [2, 3]
+-- >>> "hello world" `shouldSatisfy` P.hasSuffix " world"
 hasSuffix :: (HasSubsequences a, Monad m) => a -> Predicate m a
 hasSuffix suffix =
   Predicate
@@ -579,6 +685,9 @@
 
 {----- IO -----}
 
+-- | A predicate checking if the input is an IO action that returns a value matching the given predicate.
+--
+-- >>> pure 1 `shouldSatisfy` P.returns (P.eq 1)
 returns :: (MonadIO m) => Predicate m a -> Predicate m (m a)
 returns Predicate{..} =
   Predicate
@@ -604,6 +713,9 @@
     , predicateDispNeg = predicateDispNeg
     }
 
+-- | A predicate checking if the input is an IO action that throws an exception matching the given predicate.
+--
+-- >>> throwIO MyException `shouldSatisfy` P.throws (P.eq MyException)
 throws :: (Exception e, MonadUnliftIO m) => Predicate m e -> Predicate m (m a)
 throws Predicate{..} =
   Predicate
@@ -655,7 +767,7 @@
 -- @
 -- prop "reverse . reverse === id" $ do
 --   let genList = Gen.list (Gen.linear 0 10) $ Gen.int (Gen.linear 0 1000)
---   (reverse . reverse) === id \`shouldSatisfy\` P.isoWith genList
+--   (reverse . reverse) P.=== id \`shouldSatisfy\` P.isoWith genList
 -- @
 (===) :: (a -> b) -> (a -> b) -> IsoChecker a b
 f === g = IsoChecker (Fun "lhs" f) (Fun "rhs" g)
@@ -697,6 +809,10 @@
 
 {----- Snapshot -----}
 
+-- | A predicate checking if the input matches the snapshot.
+-- See the "Snapshot tests" section in the README.
+--
+-- >>> user `shouldSatisfy` P.matchesSnapshot
 matchesSnapshot :: (Typeable a, MonadIO m) => Predicate m a
 matchesSnapshot =
   Predicate
diff --git a/src/Skeletest/Internal/Spec.hs b/src/Skeletest/Internal/Spec.hs
--- a/src/Skeletest/Internal/Spec.hs
+++ b/src/Skeletest/Internal/Spec.hs
@@ -256,6 +256,7 @@
 
 {----- Defining a Spec -----}
 
+-- | The entity or concept being tested.
 describe :: String -> Spec -> Spec
 describe name = runIdentity . withSpecTrees (pure . (: []) . mkGroup)
   where
@@ -275,9 +276,27 @@
         , testAction = runTestable t
         }
 
+-- | Define an IO-based test.
+--
+-- Should typically be written to be read as full sentences in traditional BDD style:
+-- https://en.wikipedia.org/wiki/Behavior-driven_development.
+--
+-- @
+-- describe \"User\" $ do
+--   it "can be checked for equality" $ do
+--     user1 `shouldBe` user1
+-- @
 it :: String -> IO () -> Spec
 it = test
 
+-- | Define a property test.
+--
+-- @
+-- describe \"User\" $ do
+--   prop "decode . encode === Just" $ do
+--     let genUser = ...
+--     (decode . encode) P.=== Just \`shouldSatisfy\` P.isoWith genUser
+-- @
 prop :: String -> Property -> Spec
 prop = test
 
@@ -335,6 +354,7 @@
           Nothing -> runTest
     }
 
+-- | Mark tests as tests that should only be run when explicitly specified on the command line.
 markManual :: Spec -> Spec
 markManual = withMarker MarkerManual
 
diff --git a/src/Skeletest/Internal/TestRunner.hs b/src/Skeletest/Internal/TestRunner.hs
--- a/src/Skeletest/Internal/TestRunner.hs
+++ b/src/Skeletest/Internal/TestRunner.hs
@@ -41,7 +41,13 @@
 
 class (MonadIO m) => Testable m where
   runTestable :: m () -> IO TestResult
+
+  -- | Add any context to display if the test fails.
+  --
+  -- >>> (code, stdout) <- runCommand ...
+  -- >>> context stdout $ code `shouldBe` ExitSuccess
   context :: String -> m a -> m a
+
   throwFailure :: AssertionFail -> m a
 
 {----- TestResult -----}
diff --git a/src/Skeletest/Predicate.hs b/src/Skeletest/Predicate.hs
--- a/src/Skeletest/Predicate.hs
+++ b/src/Skeletest/Predicate.hs
@@ -16,6 +16,7 @@
   nothing,
   left,
   right,
+  list,
   tup,
   con,
 
diff --git a/src/Skeletest/Prop/Internal.hs b/src/Skeletest/Prop/Internal.hs
--- a/src/Skeletest/Prop/Internal.hs
+++ b/src/Skeletest/Prop/Internal.hs
@@ -358,10 +358,11 @@
 --
 -- In the following example, if the condition does not have at least 30%
 -- coverage, the test will fail.
+--
 -- @
 -- match <- forAll Gen.bool
--- cover 30 "True" $ match
--- cover 30 "False" $ not match
+-- cover 30 "true" $ match
+-- cover 30 "false" $ not match
 -- @
 cover :: (GHC.HasCallStack) => Double -> String -> Bool -> Property
 cover p l cond = GHC.withFrozenCallStack $ propM $ Hedgehog.cover (Hedgehog.CoverPercentage p) (Hedgehog.LabelName l) cond
diff --git a/test/Skeletest/PredicateSpec.hs b/test/Skeletest/PredicateSpec.hs
--- a/test/Skeletest/PredicateSpec.hs
+++ b/test/Skeletest/PredicateSpec.hs
@@ -89,6 +89,17 @@
         Right 1 `shouldNotSatisfy` P.right (P.gt 2)
         Left 1 `shouldNotSatisfy` P.right P.anything
 
+    describe "list" $ do
+      it "checks list" $ do
+        [1, 2, 3] `shouldSatisfy` P.list [P.eq 1, P.eq 2, P.eq 3]
+        [1, 2, 3] `shouldNotSatisfy` P.list [P.eq 1, P.eq 2, P.lt 0]
+        [1, 2, 3] `shouldNotSatisfy` P.list [P.eq 1, P.eq 2]
+        [1, 2, 3] `shouldNotSatisfy` P.list [P.eq 1, P.eq 2, P.eq 3, P.eq 4]
+
+      it "shows helpful failure messages" $ do
+        snapshotFailure (P.list [P.eq 0, P.eq 1]) [0, 10]
+        snapshotFailure (P.list [P.eq 0, P.eq 1]) [0]
+
     describe "tup" $ do
       it "checks all predicates" $ do
         (1, "hello") `shouldSatisfy` P.tup (P.eq 1, P.hasPrefix "he")
diff --git a/test/Skeletest/__snapshots__/PredicateSpec.snap.md b/test/Skeletest/__snapshots__/PredicateSpec.snap.md
--- a/test/Skeletest/__snapshots__/PredicateSpec.snap.md
+++ b/test/Skeletest/__snapshots__/PredicateSpec.snap.md
@@ -268,6 +268,28 @@
 --------------------------------------------------------------------------------
 ```
 
+## Data types / list / shows helpful failure messages
+
+```
+10 ≠ 1
+
+Expected:
+  [= 0, = 1]
+
+Got:
+  [0,10]
+```
+
+```
+Got different number of elements
+
+Expected:
+  [= 0, = 1]
+
+Got:
+  [0]
+```
+
 ## Data types / tup / shows helpful failure messages
 
 ```
