Repository: python-attrs/cattrs

Issue:
Tuple[A, B] can be structured from a list with less than two values
* cattrs version: 0.9.0
* Python version: 3.7.2
* Operating System: Win 10 x64

### Description

When structuring a list using the type `Tuple[float, float]`, cattrs will happily construct a tuple from a list with less than two values. I was expecting an error instead. Is this intentional?

### What I Did

```
In [17]: from typing import Tuple

In [18]: cattr.structure([1,2], Tuple[float, float])
Out[18]: (1.0, 2.0)

In [19]: cattr.structure([1], Tuple[float, float])
Out[19]: (1.0,)

In [20]: cattr.structure([], Tuple[float, float])
Out[20]: ()
```


Discussion:

Patch:
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..dabcacf
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,3 @@
+# These are supported funding model platforms
+
+github: Tinche
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index f15e51b..f96bc8f 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -64,11 +64,10 @@ Ready to contribute? Here's how to set up `cattrs` for local development.
 
     $ git clone git@github.com:your_name_here/cattrs.git
 
-3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development::
+3. Install your local copy into a virtualenv. Assuming you have poetry installed, this is how you set up your fork for local development::
 
-    $ mkvirtualenv cattrs
     $ cd cattrs/
-    $ pip install -e .\[dev\]
+    $ poetry install
 
 4. Create a branch for local development::
 
@@ -78,12 +77,11 @@ Ready to contribute? Here's how to set up `cattrs` for local development.
 
 5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox::
 
-    $ flake8 cattrs tests
+    $ poetry shell
+    $ flake8 src tests
     $ pytest
     $ tox
 
-   To get flake8 and tox, just pip install them into your virtualenv.
-
 6. Commit your changes and push your branch to GitHub::
 
     $ git add .
diff --git a/HISTORY.rst b/HISTORY.rst
index e20be42..93de9e6 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -16,6 +16,11 @@ History
 * Fix ``typing.Set`` applying too broadly when used with the ``GenConverter.unstruct_collection_overrides`` parameter on Python versions below 3.9. Switch to ``typing.AbstractSet`` on those versions to restore the old behavior.
   (`#264 <https://github.com/python-attrs/cattrs/issues/264>`_)
 * Uncap the required Python version, to avoid problems detailed in https://iscinumpy.dev/post/bound-version-constraints/#pinning-the-python-version-is-special (`#275 <https://github.com/python-attrs/cattrs/issues/275>`_)
+* Fix `Converter.register_structure_hook_factory` and `cattrs.gen.make_dict_unstructure_fn` type annotations.
+  (`#281 <https://github.com/python-attrs/cattrs/issues/281>`_)
+* Expose all error classes in the `cattr.errors` namespace. Note that it is deprecated, just use `cattrs.errors`. (`#252 <https://github.com/python-attrs/cattrs/issues/252>`_)
+* ``cattrs.Converter`` and ``cattrs.BaseConverter`` can now copy themselves using the ``copy`` method.
+  (`#284 <https://github.com/python-attrs/cattrs/pull/284>`_)
 
 22.1.0 (2022-04-03)
 -------------------
diff --git a/docs/converters.rst b/docs/converters.rst
index b87e1c8..47d522f 100644
--- a/docs/converters.rst
+++ b/docs/converters.rst
@@ -2,7 +2,7 @@
 Converters
 ==========
 
-All ``cattrs`` functionality is exposed through a ``cattrs.Converter`` object.
+All ``cattrs`` functionality is exposed through a :py:class:`cattrs.Converter` object.
 Global ``cattrs`` functions, such as ``cattrs.unstructure()``, use a single
 global converter. Changes done to this global converter, such as registering new
 ``structure`` and ``unstructure`` hooks, affect all code using the global
@@ -38,6 +38,9 @@ Currently, a converter contains the following state:
 * a ``dict_factory`` callable, used for creating ``dicts`` when dumping
   ``attrs`` classes using ``AS_DICT``.
 
+Converters may be cloned using the :py:attr:`cattrs.Converter.copy` method.
+The new copy may be changed through the `copy` arguments, but will retain all manually registered hooks from the original.
+
 ``cattrs.Converter``
 --------------------
 
diff --git a/docs/index.rst b/docs/index.rst
index 1160093..6eda12a 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -13,8 +13,8 @@ Contents:
 
    readme
    installation
-   usage
    converters
+   usage
    structuring
    unstructuring
    validation
diff --git a/poetry.lock b/poetry.lock
index ebbf56e..6c5dfc4 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -16,17 +16,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
 [[package]]
 name = "attrs"
-version = "21.4.0"
+version = "22.1.0"
 description = "Classes Without Boilerplate"
 category = "main"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+python-versions = ">=3.5"
 
 [package.extras]
-dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
+dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
 docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
-tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
-tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
+tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
+tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"]
 
 [[package]]
 name = "babel"
@@ -764,7 +764,7 @@ python-versions = ">=3.6"
 
 [[package]]
 name = "ujson"
-version = "5.2.0"
+version = "5.4.0"
 description = "Ultra fast JSON encoder and decoder for Python"
 category = "dev"
 optional = false
@@ -816,8 +816,8 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-
 
 [metadata]
 lock-version = "1.1"
-python-versions = "^3.7"
-content-hash = "68ab16f23e95ceeab6b8b683c6e84991fb917c391e1f80d7e56733bf9fbd7767"
+python-versions = ">= 3.7"
+content-hash = "4677a1b80c748493c693912cb9b7d30fdb33dd0fa7ee961c3ee1dfd6a1e01699"
 
 [metadata.files]
 alabaster = [
@@ -829,17 +829,14 @@ atomicwrites = [
     {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
 ]
 attrs = [
-    {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
-    {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
+    {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
+    {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
 ]
 babel = [
     {file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"},
     {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"},
 ]
-beautifulsoup4 = [
-    {file = "beautifulsoup4-4.11.1-py3-none-any.whl", hash = "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30"},
-    {file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"},
-]
+beautifulsoup4 = []
 black = [
     {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"},
     {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"},
@@ -952,10 +949,7 @@ hypothesis = [
     {file = "hypothesis-6.43.1-py3-none-any.whl", hash = "sha256:a4e30f0c787e23ac3081eae5ce327b92b5125010f000403d4b26b74061d8029b"},
     {file = "hypothesis-6.43.1.tar.gz", hash = "sha256:b0ef5f82bb925e540f5d26e7e06280fa88113a763cc3e6832c93301766fa6900"},
 ]
-idna = [
-    {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
-    {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
-]
+idna = []
 imagesize = [
     {file = "imagesize-1.3.0-py2.py3-none-any.whl", hash = "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c"},
     {file = "imagesize-1.3.0.tar.gz", hash = "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d"},
@@ -1091,6 +1085,7 @@ orjson = [
     {file = "orjson-3.6.8-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:ccb356a47ab1067cd3549847e9db1d279a63fe0482d315b3ffd6e7abef35ef77"},
     {file = "orjson-3.6.8-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab29c069c222248ce302a25855b4e1664f9436e8ae5a131fb0859daf31676d2b"},
     {file = "orjson-3.6.8-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d2b5e4cba9e774ac011071d9d27760f97f4b8cd46003e971d122e712f971345"},
+    {file = "orjson-3.6.8-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:c311ec504414d22834d5b972a209619925b48263856a11a14d90230f9682d49c"},
     {file = "orjson-3.6.8-cp37-cp37m-manylinux_2_24_x86_64.whl", hash = "sha256:a3dfec7950b90fb8d143743503ee53fa06b32e6068bdea792fc866284da3d71d"},
     {file = "orjson-3.6.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b890dbbada2cbb26eb29bd43a848426f007f094bb0758df10dfe7a438e1cb4b4"},
     {file = "orjson-3.6.8-cp37-none-win_amd64.whl", hash = "sha256:9143ae2c52771525be9ad11a7a8cc8e7fd75391b107e7e644a9e0050496f6b4f"},
@@ -1098,6 +1093,7 @@ orjson = [
     {file = "orjson-3.6.8-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:90159ea8b9a5a2a98fa33dc7b421cfac4d2ae91ba5e1058f5909e7f059f6b467"},
     {file = "orjson-3.6.8-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:656fbe15d9ef0733e740d9def78f4fdb4153102f4836ee774a05123499005931"},
     {file = "orjson-3.6.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7be3be6153843e0f01351b1313a5ad4723595427680dac2dfff22a37e652ce02"},
+    {file = "orjson-3.6.8-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:dd24f66b6697ee7424f7da575ec6cbffc8ede441114d53470949cda4d97c6e56"},
     {file = "orjson-3.6.8-cp38-cp38-manylinux_2_24_x86_64.whl", hash = "sha256:b07c780f7345ecf5901356dc21dee0669defc489c38ce7b9ab0f5e008cc0385c"},
     {file = "orjson-3.6.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ea32015a5d8a4ce00d348a0de5dc7040e0ad58f970a8fcbb5713a1eac129e493"},
     {file = "orjson-3.6.8-cp38-none-win_amd64.whl", hash = "sha256:c5a3e382194c838988ec128a26b08aa92044e5e055491cc4056142af0c1c54d7"},
@@ -1290,14 +1286,8 @@ pytest-benchmark = [
     {file = "pytest-benchmark-3.4.1.tar.gz", hash = "sha256:40e263f912de5a81d891619032983557d62a3d85843f9a9f30b98baea0cd7b47"},
     {file = "pytest_benchmark-3.4.1-py2.py3-none-any.whl", hash = "sha256:36d2b08c4882f6f997fd3126a3d6dfd70f3249cde178ed8bbc0b73db7c20f809"},
 ]
-python-dateutil = [
-    {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
-    {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
-]
-pytz = [
-    {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"},
-    {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"},
-]
+python-dateutil = []
+pytz = []
 pytzdata = [
     {file = "pytzdata-2020.1-py2.py3-none-any.whl", hash = "sha256:e1e14750bcf95016381e4d472bad004eef710f2d6417240904070b3d6654485f"},
     {file = "pytzdata-2020.1.tar.gz", hash = "sha256:3efa13b335a00a8de1d345ae41ec78dd11c9f8807f522d39850f2dd828681540"},
@@ -1337,10 +1327,7 @@ requests = [
     {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},
     {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"},
 ]
-six = [
-    {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
-    {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
-]
+six = []
 snowballstemmer = [
     {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"},
     {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"},
@@ -1349,10 +1336,7 @@ sortedcontainers = [
     {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"},
     {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"},
 ]
-soupsieve = [
-    {file = "soupsieve-2.3.2.post1-py3-none-any.whl", hash = "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759"},
-    {file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"},
-]
+soupsieve = []
 sphinx = [
     {file = "Sphinx-4.5.0-py3-none-any.whl", hash = "sha256:ebf612653238bcc8f4359627a9b7ce44ede6fdd75d9d30f68255c7383d3a6226"},
     {file = "Sphinx-4.5.0.tar.gz", hash = "sha256:7bf8ca9637a4ee15af412d1a1d9689fec70523a68ca9bb9127c2f3eeb344e2e6"},
@@ -1428,56 +1412,56 @@ typing-extensions = [
     {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"},
 ]
 ujson = [
-    {file = "ujson-5.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:754e9da96a24535ae5ab2a52e1d1dfc65a6a717c14063855b83f327fdf2173ea"},
-    {file = "ujson-5.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6227597d0201ceadc902d1a8edaffaeb244050b197368ed25e6f6be0df170a6f"},
-    {file = "ujson-5.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be909514a47b6272e34cd1213feee324ca35a354e07f1ae3aba12d3694a5279f"},
-    {file = "ujson-5.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:489d495431c80dc0048c4551a0d6cdbf1209e2d274f47c3f72415c91842eeb68"},
-    {file = "ujson-5.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4d1ed3897e45477b2a4a1371186df299b13938d4d44d850953a4bb0ea4cb38f3"},
-    {file = "ujson-5.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:102b8eb5e15e6c5537426414d180c28dbf0489e51f7c22b706511ac84aae4458"},
-    {file = "ujson-5.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2c04456de1fc92cc7062904c176c74e6ea220469b949508be42e819646a28457"},
-    {file = "ujson-5.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a3c6798035b574ceba747de83f3223a622622b7ab77a24f8b4fbea2cb92f14b0"},
-    {file = "ujson-5.2.0-cp310-cp310-win32.whl", hash = "sha256:27a254a150e46980608b16ef3b609e703173492cfa738f4644c81d7e7d77494c"},
-    {file = "ujson-5.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:8c3f7578a62d9255650ef32e78d3345e98262e064c9ba3f205311b4c9eb507a6"},
-    {file = "ujson-5.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:584c558c23ddc21f5b07d2c54ee527731bd9716101c27829023ab7f3ffbaa8fc"},
-    {file = "ujson-5.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d57a87bbc77d66b8a2b74bab66357c3bb6194f5d248f1053fb8044787abde73f"},
-    {file = "ujson-5.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb4555df1fe018806ba14cc38786269c8e213930103c6d0ac81e506d09d1de7e"},
-    {file = "ujson-5.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d38c2a58c892c680080b22b59eebd77b7c6f4ae24361111fba115f9ed3651dcf"},
-    {file = "ujson-5.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a6f3ad3b11578bc4e25d5bd256c938fe2c7c015d8f504bc7835f127ed26a0818"},
-    {file = "ujson-5.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b5fcbaabf3d115cb816eb165f3fa5de5c5bc795473a554ae55620d134ddf2d36"},
-    {file = "ujson-5.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a1a55b3310632661a03ce68ccfb92264031aea21626d6fa5c8f6c32e769be7b6"},
-    {file = "ujson-5.2.0-cp37-cp37m-win32.whl", hash = "sha256:04a8c388b2d16316df3365c81f368955662581f6a4ff033e9aba2dd1ffc9e05e"},
-    {file = "ujson-5.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d2357ce7d93eadd29b6efbe72228809948cc59ec6682c20fa6de08aeef1703f8"},
-    {file = "ujson-5.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e53388fb092197cb8f956673792aca994872917d897ca42a0abf7a35e293575a"},
-    {file = "ujson-5.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dc71ead5706e81fdf1054c8c11e4aaab43527da450a2701213c20717852d1a51"},
-    {file = "ujson-5.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:350a3010db0045e1306bbdf889d1bdaee9bb095856c317716f0a74108cf4afe9"},
-    {file = "ujson-5.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acc874128baddeff908736db251597e4cbd007a384730377a59a61b08886599"},
-    {file = "ujson-5.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c549d5a7652c3a0dd00ef6ff910fb01878bc116c66c94ac455a55cffa32cc229"},
-    {file = "ujson-5.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ed78a5b169ece75a1e1368935ce6ab051dcbcd5c158b9796b2f1fa6cc467a651"},
-    {file = "ujson-5.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:468d7d8dcbafc3fd40cc73e4a533a7a1d4f935f605c15ae6cac32c6d53c4c6aa"},
-    {file = "ujson-5.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dc5fd1d5b48edd3cc64e89ea94abe231509fdc938bdeafafe9aef3a05810159f"},
-    {file = "ujson-5.2.0-cp38-cp38-win32.whl", hash = "sha256:49ce8521b0cdf210481bd89887fd1bd0a975f66088b1256dafc77c67c8ccb89d"},
-    {file = "ujson-5.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:c519743a53bbe8aac6b743bcf50eb83057d1e0341e1ca8f8491f729a885af640"},
-    {file = "ujson-5.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b3671e1dfc49a4b4453d89fd7438aa9d7cca28afe329c70eba84e2a5778dbf3f"},
-    {file = "ujson-5.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11f735870f189bff1841c720115226894415ab6a7796dee8ab46bc767ea2e743"},
-    {file = "ujson-5.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90de04391916c5adc7bbcc69bd778e263ed45cc83c070099cb07ed25068d6a12"},
-    {file = "ujson-5.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c7712da662b92f80442a8efc0df09cea3a5efb42b0dd6a642e36b1b40a260d4"},
-    {file = "ujson-5.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9b1c3d2b22c040a81ff4e5927ce307919f7ac8bf888afded714d925edc8d0a4"},
-    {file = "ujson-5.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c5bbe6de6c9a5fe8dca56e36fb5c4a42e1a01d4aae1ac20cd8d7d82ccff9430"},
-    {file = "ujson-5.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:940f35e9a0969440621445dbb6adffaa2cea77d0262abc74fce78704120c4534"},
-    {file = "ujson-5.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6677bee8690c71f5e6cf519a6d8400f04fbd3ff9f6c50f35f1b664bc94546f84"},
-    {file = "ujson-5.2.0-cp39-cp39-win32.whl", hash = "sha256:0b47a138203bb06bdac03b2a89ac9b2993fd32cb7daded06c966dd84300a5786"},
-    {file = "ujson-5.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:e991b7b3a08ac9e9d3a51589ef1c359c8d44ece730351cfac055684bf3787372"},
-    {file = "ujson-5.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d1e5c635b7c3465ab8d2e3dc97c341ef1801c53a378f1d1d4cb934f6c90ec66c"},
-    {file = "ujson-5.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef868bf01851869a26c0ca5f88036903836c3a6b463c74d96b37f294f6bdeea4"},
-    {file = "ujson-5.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5e374e793b0a3c7df20ee4c8234e89859ddb2b2821cc3300ae94ab5b08fa6d0"},
-    {file = "ujson-5.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:080da13f81740c076e5f16c254a10d0e32f45d225a5e6b0687a86493cfcfbafb"},
-    {file = "ujson-5.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:75a886bd89d8e5a004a39a6c5dc8a43bb7fcf05129d2dccd16a59602a612823a"},
-    {file = "ujson-5.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:54ee7c46615b42f7ae9dca90f54d204a4d2041a4c926b08fffa953aa3a246e54"},
-    {file = "ujson-5.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b455a62bd20e890b2124a65df45313b4292dbea851ef38574e5e2de94691ad5"},
-    {file = "ujson-5.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1a619bad9894dad144184b735c98179c7d92d7b40fbda28eb8b0857bdfdf52"},
-    {file = "ujson-5.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:729af63e4de30c54b527b54b4100266f79833c1e8ba35e784f01b44c2aca88d8"},
-    {file = "ujson-5.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:25522c674b35c33f375586ac98d92ce731e79059424507ecbccbfcbce832d597"},
-    {file = "ujson-5.2.0.tar.gz", hash = "sha256:163191b88842d874e081707d35de2e205e0e396e70fd068d1038879bca8b17ad"},
+    {file = "ujson-5.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:511aa641a5b91d19280183b134fb6c473039d4dd82e987ac810cffba783521ac"},
+    {file = "ujson-5.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b045ca5497a950cc3492840adb3bcb3b9e305ed6599ed14c6aeaa08011aa463f"},
+    {file = "ujson-5.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa00b746138835271653b0c3da171d2a8b510c579381f71e8b8e03484d50d825"},
+    {file = "ujson-5.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91edcf9978ee401119e9c8589376ae37fd3e6e75ee365c49385cb005eaff1535"},
+    {file = "ujson-5.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05e411627e5d6ee773232960ca7307e66017f78e3fa74f7e95c3a8cc5cb05415"},
+    {file = "ujson-5.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7d4c9ccd30e621e714ec24ca911ad8873567dc1ac1e5e914405ea9dd16b9d40c"},
+    {file = "ujson-5.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:dd0d4ec694cab8a0a4d85f45f81ae0065465c4670f0db72ba48d6c4e7ae42834"},
+    {file = "ujson-5.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:aaa77af91df3f71858a1f792c74d3f2d3abf3875f93ab1a2b9a24b3797743b02"},
+    {file = "ujson-5.4.0-cp310-cp310-win32.whl", hash = "sha256:fbea46c0fbc1c3bc8f957afd8dbb25b4ea3a356e18ee6dd79ace6cf32bd4cff7"},
+    {file = "ujson-5.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:2d98248f1df1e1aab67e0374ab98945dd36bc1764753d71fd8aea5f296360b76"},
+    {file = "ujson-5.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f5c547d49a7e9d3f231e9323171bbbbcef63173fb007a2787cd4f05ac6269315"},
+    {file = "ujson-5.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39bb702ca1612253b5e4b6004e0f20208c98a446606aa351f9a7ba5ceaff0eb8"},
+    {file = "ujson-5.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:381c97d326d1ec569d318cc0ae83940ea2df125ede1000871680fefd5b7fdea9"},
+    {file = "ujson-5.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a0707f381f97e1287c0dbf94d95bd6c0bbf6e4eeeaa656f0076b7883010c818"},
+    {file = "ujson-5.4.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6a20f2f6e8818c1ab89dd4be6bbad3fc2ddb15287f89e7ea35f3eb849afebbd9"},
+    {file = "ujson-5.4.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8cd6117e33233f2de6bc896eea6a5a59b58a37db08f371157264e0ec5e51c76a"},
+    {file = "ujson-5.4.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:31bdb6d771d5ef6d37134b42211500bfe176c55d399f3317e569783dc42ed38e"},
+    {file = "ujson-5.4.0-cp37-cp37m-win32.whl", hash = "sha256:baa76a6f707a6d22437fe9c7ec9719672fb04d4d9435a3e80ee9b1aaeb2089d9"},
+    {file = "ujson-5.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ee29cf5cfc1e841708297633e1ce749aa851fb96830bbe51f2e5940741ff2441"},
+    {file = "ujson-5.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e12272361e9722777c83b3f5b0bb91d402531f36e80c6e5fafb6acb89e897e3"},
+    {file = "ujson-5.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3212847d3885bfd4f5fd56cdc37645a8f8e8a80d6cb569505da22fd9eb0e1a02"},
+    {file = "ujson-5.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcde3135265ecdd5714a7de4fdc167925390d7b17ca325e59980f4114c962b8"},
+    {file = "ujson-5.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0551c1ba0bc9e05b69d9c18266dbc93252b5fa3cd9940051bc88a0dd33607b19"},
+    {file = "ujson-5.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13297a7d501f9c8c53e409d4fa57cc574e4fbfbe8807ef2c4c7ce2e3ec933a85"},
+    {file = "ujson-5.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2974b17bc522ef86d98b498959d82f03c02e07d9eb08746026415298f4a4bca3"},
+    {file = "ujson-5.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5df8b6369ee5ee2685fcc917f6c46b34e599c6e9a512fada6dfd752b909fa06a"},
+    {file = "ujson-5.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:754f422aba8db8201a1073f25e2f732effc6471f8755708b16e6ebf19dd23634"},
+    {file = "ujson-5.4.0-cp38-cp38-win32.whl", hash = "sha256:ea7fbc540bc04d5b05e5cd54e60ee8745ac665eedf2bad2ba9d12d5c7a7b7d2e"},
+    {file = "ujson-5.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:8d472efa9c92e1b2933a22d2f1dbd5237087997136b24ac2b913bf4e8be03135"},
+    {file = "ujson-5.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e2a9ddb5c6d1427056b8d62a1a172a18ae522b14d9ba5996b8281b09cba87edd"},
+    {file = "ujson-5.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1120c8263f7d85e89533a2b46d80cc6def15114772010ede4d197739e111dba6"},
+    {file = "ujson-5.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:400e4ca8a59f71398e8fa56c4d2d6f535e2a121ddb57284ec15752ffce2dd63a"},
+    {file = "ujson-5.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e844be0831042aa91e847e5ab03bddd1089ab1a8dd0a1bf90411abf864f058b2"},
+    {file = "ujson-5.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b46aee21e5d75426c4058dfdb42f7e7b1d130c664ee5027a8dbbc50872dc32b"},
+    {file = "ujson-5.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:326a96324ed9215b0bc9f1a5af324fb33900b6b0901516bcc421475d6596de0d"},
+    {file = "ujson-5.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:fd82932aaa224abd7d01e823b77aef9970f5ac1695027331d99e7f5fda9d37f5"},
+    {file = "ujson-5.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8cce79ce47c37132373fbdf55b683883c262a3a60763130e080b8394c1201d32"},
+    {file = "ujson-5.4.0-cp39-cp39-win32.whl", hash = "sha256:191f88d5865740497b9827ef9b7c12f37a79872ac984e09f0901a10024019380"},
+    {file = "ujson-5.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:68c7f753aec490c6566fd3cd301887c413ac3a588316e446f30a4134ac665668"},
+    {file = "ujson-5.4.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a2e645325f844f9c890c9d956fc2d35ca91f38c857278238ef6516c2f99cf7c"},
+    {file = "ujson-5.4.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cec010d318a0238b1333ea9f40d5603d374cc026c29c4471e2661712c6682da1"},
+    {file = "ujson-5.4.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b40a3757a563ef77c3f2f9ea1732c2924e8b3b2bda3fa89513f949472ad40b6e"},
+    {file = "ujson-5.4.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67f4e2fa81e1d99c01e7b1978ab0cbf3c9a8b663f683a709f87baad110d5b940"},
+    {file = "ujson-5.4.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:9ae1d0094ce730e39e09656bc14074d9573cdd80adec1a55b06d8bf1f9613a01"},
+    {file = "ujson-5.4.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:784dbd12925845a3f0757a956447e2fd31418abb5aeaebf3aca1203195f16fd1"},
+    {file = "ujson-5.4.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422653083c6df6cec17fdb5d6106c209aad9b0c94131c53b073980403db22167"},
+    {file = "ujson-5.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e91947fda8354ea7faf698b084ebcdbabd239e7b15d8436fb74394f59a207ac9"},
+    {file = "ujson-5.4.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef985eb2770900a485431910bd3f333b56d1a34b65f8c26a6ed8e8adf55f98d9"},
+    {file = "ujson-5.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:025758cf6561af6986d77cd4af9367ab56dde5c7c50f13f59e6964b4b25df73e"},
+    {file = "ujson-5.4.0.tar.gz", hash = "sha256:6b953e09441e307504130755e5bd6b15850178d591f66292bba4608c4f7f9b00"},
 ]
 urllib3 = [
     {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"},
diff --git a/pyproject.toml b/pyproject.toml
index e62303e..c0ec72e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -38,7 +38,7 @@ pendulum = "^2.1.2"
 isort = "^5.8.0"
 black = "^22.3.0"
 immutables = "^0.15"
-ujson = "^5.1.0"
+ujson = "^5.4.0"
 orjson = { version = "^3.5.2", markers = "implementation_name == 'cpython'" }
 msgpack = "^1.0.2"
 PyYAML = "^5.4.1"
diff --git a/src/cattr/errors.py b/src/cattr/errors.py
index ef624ed..af092e9 100644
--- a/src/cattr/errors.py
+++ b/src/cattr/errors.py
@@ -1,3 +1,15 @@
-from cattrs.errors import StructureHandlerNotFoundError
+from cattrs.errors import (
+    BaseValidationError,
+    ClassValidationError,
+    ForbiddenExtraKeysError,
+    IterableValidationError,
+    StructureHandlerNotFoundError,
+)
 
-__all__ = ["StructureHandlerNotFoundError"]
+__all__ = [
+    "BaseValidationError",
+    "ClassValidationError",
+    "ForbiddenExtraKeysError",
+    "IterableValidationError",
+    "StructureHandlerNotFoundError",
+]
diff --git a/src/cattrs/__init__.py b/src/cattrs/__init__.py
index c4bd080..f8b8801 100644
--- a/src/cattrs/__init__.py
+++ b/src/cattrs/__init__.py
@@ -1,25 +1,37 @@
 from .converters import BaseConverter, Converter, GenConverter, UnstructureStrategy
+from .errors import (
+    BaseValidationError,
+    ClassValidationError,
+    ForbiddenExtraKeysError,
+    IterableValidationError,
+    StructureHandlerNotFoundError,
+)
 from .gen import override
 
 __all__ = (
     "BaseConverter",
+    "BaseValidationError",
+    "ClassValidationError",
     "Converter",
     "converters",
     "disambiguators",
     "dispatch",
     "errors",
+    "ForbiddenExtraKeysError",
     "gen",
     "GenConverter",
     "global_converter",
+    "IterableValidationError",
     "override",
     "preconf",
-    "register_structure_hook_func",
     "register_structure_hook",
-    "register_unstructure_hook_func",
+    "register_structure_hook_func",
     "register_unstructure_hook",
+    "register_unstructure_hook_func",
+    "structure",
     "structure_attrs_fromdict",
     "structure_attrs_fromtuple",
-    "structure",
+    "StructureHandlerNotFoundError",
     "unstructure",
     "UnstructureStrategy",
 )
diff --git a/src/cattrs/converters.py b/src/cattrs/converters.py
index 0b1bf94..0a6b065 100644
--- a/src/cattrs/converters.py
+++ b/src/cattrs/converters.py
@@ -146,7 +146,7 @@ class BaseConverter:
         # Per-instance register of to-attrs converters.
         # Singledispatch dispatches based on the first argument, so we
         # store the function and switch the arguments in self.loads.
-        self._structure_func = MultiStrategyDispatch(self._structure_error)
+        self._structure_func = MultiStrategyDispatch(BaseConverter._structure_error)
         self._structure_func.register_func_list(
             [
                 (lambda cl: cl is Any or cl is Optional or cl is None, lambda v, _: v),
@@ -264,7 +264,7 @@ class BaseConverter:
     def register_structure_hook_factory(
         self,
         predicate: Callable[[Any], bool],
-        factory: Callable[[Any], Callable[[Any], Any]],
+        factory: Callable[[Any], Callable[[Any, Any], Any]],
     ) -> None:
         """
         Register a hook factory for a given predicate.
@@ -309,7 +309,8 @@ class BaseConverter:
         """Convert an enum to its value."""
         return obj.value
 
-    def _unstructure_identity(self, obj):
+    @staticmethod
+    def _unstructure_identity(obj):
         """Just pass it through."""
         return obj
 
@@ -340,7 +341,8 @@ class BaseConverter:
 
     # Python primitives to classes.
 
-    def _structure_error(self, _, cl):
+    @staticmethod
+    def _structure_error(_, cl):
         """At the bottom of the condition stack, we explode if we can't handle it."""
         msg = "Unsupported type: {0!r}. Register a structure hook for " "it.".format(cl)
         raise StructureHandlerNotFoundError(msg, type_=cl)
@@ -573,6 +575,14 @@ class BaseConverter:
                 return tuple(conv(e, tup_type) for e in obj)
         else:
             # We're dealing with a heterogenous tuple.
+            exp_len = len(tup_params)
+            try:
+                len_obj = len(obj)
+            except TypeError:
+                pass  # most likely an unsized iterator, eg generator
+            else:
+                if len_obj > exp_len:
+                    exp_len = len_obj
             if self.detailed_validation:
                 errors = []
                 res = []
@@ -583,18 +593,31 @@ class BaseConverter:
                     except Exception as exc:
                         exc.__note__ = f"Structuring {tup} @ index {ix}"
                         errors.append(exc)
+                if len(res) < exp_len:
+                    problem = "Not enough" if len(res) < len(tup_params) else "Too many"
+                    exc = ValueError(
+                        f"{problem} values in {obj!r} to structure as {tup!r}"
+                    )
+                    exc.__note__ = f"Structuring {tup}"
+                    errors.append(exc)
                 if errors:
                     raise IterableValidationError(
                         f"While structuring {tup!r}", errors, tup
                     )
                 return tuple(res)
             else:
-                return tuple(
+                res = tuple(
                     [
                         self._structure_func.dispatch(t)(e, t)
                         for t, e in zip(tup_params, obj)
                     ]
                 )
+                if len(res) < exp_len:
+                    problem = "Not enough" if len(res) < len(tup_params) else "Too many"
+                    raise ValueError(
+                        f"{problem} values in {obj!r} to structure as {tup!r}"
+                    )
+                return res
 
     @staticmethod
     def _get_dis_func(union) -> Callable[..., Type]:
@@ -615,6 +638,38 @@ class BaseConverter:
             )
         return create_uniq_field_dis_func(*union_types)
 
+    def __deepcopy__(self, _) -> "BaseConverter":
+        return self.copy()
+
+    def copy(
+        self,
+        dict_factory: Optional[Callable[[], Any]] = None,
+        unstruct_strat: Optional[UnstructureStrategy] = None,
+        prefer_attrib_converters: Optional[bool] = None,
+        detailed_validation: Optional[bool] = None,
+    ) -> "BaseConverter":
+        res = self.__class__(
+            dict_factory if dict_factory is not None else self._dict_factory,
+            unstruct_strat
+            if unstruct_strat is not None
+            else (
+                UnstructureStrategy.AS_DICT
+                if self._unstructure_attrs == self.unstructure_attrs_asdict
+                else UnstructureStrategy.AS_TUPLE
+            ),
+            prefer_attrib_converters
+            if prefer_attrib_converters is not None
+            else self._prefer_attrib_converters,
+            detailed_validation
+            if detailed_validation is not None
+            else self.detailed_validation,
+        )
+
+        self._unstructure_func.copy_to(res._unstructure_func)
+        self._structure_func.copy_to(res._structure_func)
+
+        return res
+
 
 class Converter(BaseConverter):
     """A converter which generates specialized un/structuring functions."""
@@ -624,6 +679,8 @@ class Converter(BaseConverter):
         "forbid_extra_keys",
         "type_overrides",
         "_unstruct_collection_overrides",
+        "_struct_copy_skip",
+        "_unstruct_copy_skip",
     )
 
     def __init__(
@@ -736,6 +793,10 @@ class Converter(BaseConverter):
             lambda t: get_newtype_base(t) is not None, self.get_structure_newtype
         )
 
+        # We keep these so we can more correctly copy the hooks.
+        self._struct_copy_skip = self._structure_func.get_num_fns()
+        self._unstruct_copy_skip = self._unstructure_func.get_num_fns()
+
     def get_structure_newtype(self, type: Type[T]) -> Callable[[Any, Any], T]:
         base = get_newtype_base(type)
         return self._structure_func.dispatch(base)
@@ -750,7 +811,9 @@ class Converter(BaseConverter):
         h = self._structure_func.dispatch(origin)
         return h
 
-    def gen_unstructure_attrs_fromdict(self, cl: Type[T]) -> Dict[str, Any]:
+    def gen_unstructure_attrs_fromdict(
+        self, cl: Type[T]
+    ) -> Callable[[T], Dict[str, Any]]:
         origin = get_origin(cl)
         attribs = fields(origin or cl)
         if attrs_has(cl) and any(isinstance(a.type, str) for a in attribs):
@@ -767,7 +830,9 @@ class Converter(BaseConverter):
         )
         return h
 
-    def gen_structure_attrs_fromdict(self, cl: Type[T]) -> T:
+    def gen_structure_attrs_fromdict(
+        self, cl: Type[T]
+    ) -> Callable[[Mapping[str, Any], Any], T]:
         attribs = fields(get_origin(cl) if is_generic(cl) else cl)
         if attrs_has(cl) and any(isinstance(a.type, str) for a in attribs):
             # PEP 563 annotations - need to be resolved.
@@ -832,5 +897,48 @@ class Converter(BaseConverter):
         self._structure_func.register_cls_list([(cl, h)], direct=True)
         return h
 
+    def copy(
+        self,
+        dict_factory: Optional[Callable[[], Any]] = None,
+        unstruct_strat: Optional[UnstructureStrategy] = None,
+        omit_if_default: Optional[bool] = None,
+        forbid_extra_keys: Optional[bool] = None,
+        type_overrides: Optional[Mapping[Type, AttributeOverride]] = None,
+        unstruct_collection_overrides: Optional[Mapping[Type, Callable]] = None,
+        prefer_attrib_converters: Optional[bool] = None,
+        detailed_validation: Optional[bool] = None,
+    ) -> "Converter":
+        res = self.__class__(
+            dict_factory if dict_factory is not None else self._dict_factory,
+            unstruct_strat
+            if unstruct_strat is not None
+            else (
+                UnstructureStrategy.AS_DICT
+                if self._unstructure_attrs == self.unstructure_attrs_asdict
+                else UnstructureStrategy.AS_TUPLE
+            ),
+            omit_if_default if omit_if_default is not None else self.omit_if_default,
+            forbid_extra_keys
+            if forbid_extra_keys is not None
+            else self.forbid_extra_keys,
+            type_overrides if type_overrides is not None else self.type_overrides,
+            unstruct_collection_overrides
+            if unstruct_collection_overrides is not None
+            else self._unstruct_collection_overrides,
+            prefer_attrib_converters
+            if prefer_attrib_converters is not None
+            else self._prefer_attrib_converters,
+            detailed_validation
+            if detailed_validation is not None
+            else self.detailed_validation,
+        )
+
+        self._unstructure_func.copy_to(
+            res._unstructure_func, skip=self._unstruct_copy_skip
+        )
+        self._structure_func.copy_to(res._structure_func, skip=self._struct_copy_skip)
+
+        return res
+
 
 GenConverter = Converter
diff --git a/src/cattrs/dispatch.py b/src/cattrs/dispatch.py
index 42e6719..b131b63 100644
--- a/src/cattrs/dispatch.py
+++ b/src/cattrs/dispatch.py
@@ -48,7 +48,7 @@ class MultiStrategyDispatch:
 
         return self._function_dispatch.dispatch(cl)
 
-    def register_cls_list(self, cls_and_handler, direct: bool = False):
+    def register_cls_list(self, cls_and_handler, direct: bool = False) -> None:
         """Register a class to direct or singledispatch."""
         for cls, handler in cls_and_handler:
             if direct:
@@ -89,6 +89,14 @@ class MultiStrategyDispatch:
         self._direct_dispatch.clear()
         self.dispatch.cache_clear()
 
+    def get_num_fns(self) -> int:
+        return self._function_dispatch.get_num_fns()
+
+    def copy_to(self, other: "MultiStrategyDispatch", skip: int = 0):
+        self._function_dispatch.copy_to(other._function_dispatch, skip=skip)
+        for cls, fn in self._single_dispatch.registry.items():
+            other._single_dispatch.register(cls, fn)
+
 
 @attr.s(slots=True)
 class FunctionDispatch:
@@ -125,3 +133,9 @@ class FunctionDispatch:
         raise StructureHandlerNotFoundError(
             f"unable to find handler for {typ}", type_=typ
         )
+
+    def get_num_fns(self) -> int:
+        return len(self._handler_pairs)
+
+    def copy_to(self, other: "FunctionDispatch", skip: int = 0):
+        other._handler_pairs.extend(self._handler_pairs[skip:])
diff --git a/src/cattrs/gen.py b/src/cattrs/gen.py
index fbe5346..95cc02e 100644
--- a/src/cattrs/gen.py
+++ b/src/cattrs/gen.py
@@ -36,7 +36,11 @@ class AttributeOverride:
     omit: bool = False  # Omit the field completely.
 
 
-def override(omit_if_default=None, rename=None, omit: bool = False):
+def override(
+    omit_if_default: Optional[bool] = None,
+    rename: Optional[str] = None,
+    omit: bool = False,
+):
     return AttributeOverride(omit_if_default=omit_if_default, rename=rename, omit=omit)
 
 
@@ -46,12 +50,12 @@ T = TypeVar("T")
 
 
 def make_dict_unstructure_fn(
-    cl,
-    converter,
+    cl: Type[T],
+    converter: "BaseConverter",
     _cattrs_omit_if_default: bool = False,
     _cattrs_use_linecache: bool = True,
-    **kwargs,
-) -> Callable[[Any], Dict]:
+    **kwargs: AttributeOverride,
+) -> Callable[[T], Dict[str, Any]]:
     """
     Generate a specialized dict unstructuring function for an attrs class or a
     dataclass.


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
index 20e6dbd..9d67846 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,6 +1,9 @@
 import os
 
 from hypothesis import HealthCheck, settings
+from hypothesis.strategies import just, one_of
+
+from cattrs import UnstructureStrategy
 
 settings.register_profile(
     "CI", settings(suppress_health_check=[HealthCheck.too_slow]), deadline=None
@@ -8,3 +11,5 @@ settings.register_profile(
 
 if "CI" in os.environ:
     settings.load_profile("CI")
+
+unstructure_strats = one_of(just(s) for s in UnstructureStrategy)
diff --git a/tests/test_copy.py b/tests/test_copy.py
new file mode 100644
index 0000000..7868e68
--- /dev/null
+++ b/tests/test_copy.py
@@ -0,0 +1,201 @@
+from collections import OrderedDict
+from copy import deepcopy
+from typing import Callable, Type
+
+from attr import define
+from hypothesis import given
+from hypothesis.strategies import just, one_of
+from pytest import raises
+
+from cattrs import BaseConverter, Converter, UnstructureStrategy
+from cattrs.errors import ClassValidationError
+
+from . import unstructure_strats
+
+
+@define
+class Simple:
+    a: int
+
+
+@given(strat=unstructure_strats, detailed_validation=..., prefer_attrib=...)
+def test_deepcopy(
+    converter_cls: Type[BaseConverter],
+    strat: UnstructureStrategy,
+    prefer_attrib: bool,
+    detailed_validation: bool,
+):
+    c = converter_cls(
+        unstruct_strat=strat,
+        prefer_attrib_converters=prefer_attrib,
+        detailed_validation=detailed_validation,
+    )
+
+    copy = deepcopy(c)
+
+    assert c is not copy
+
+    assert c.unstructure(Simple(1)) == copy.unstructure(Simple(1))
+    assert c.detailed_validation == copy.detailed_validation
+    assert c._prefer_attrib_converters == copy._prefer_attrib_converters
+
+
+@given(
+    strat=unstructure_strats,
+    detailed_validation=...,
+    prefer_attrib=...,
+    dict_factory=one_of(just(dict), just(OrderedDict)),
+)
+def test_copy(
+    converter_cls: Type[BaseConverter],
+    strat: UnstructureStrategy,
+    prefer_attrib: bool,
+    detailed_validation: bool,
+    dict_factory: Callable,
+):
+    c = converter_cls(
+        unstruct_strat=strat,
+        prefer_attrib_converters=prefer_attrib,
+        detailed_validation=detailed_validation,
+        dict_factory=dict_factory,
+    )
+
+    copy = c.copy()
+
+    assert c is not copy
+
+    assert c.unstructure(Simple(1)) == copy.unstructure(Simple(1))
+    assert c.detailed_validation == copy.detailed_validation
+    assert c._prefer_attrib_converters == copy._prefer_attrib_converters
+    assert c._dict_factory == copy._dict_factory
+
+
+@given(
+    strat=unstructure_strats,
+    detailed_validation=...,
+    prefer_attrib=...,
+    dict_factory=one_of(just(dict), just(OrderedDict)),
+    omit_if_default=...,
+)
+def test_copy_converter(
+    strat: UnstructureStrategy,
+    prefer_attrib: bool,
+    detailed_validation: bool,
+    dict_factory: Callable,
+    omit_if_default: bool,
+):
+    """cattrs.Converter can be copied, and keeps its attributs."""
+    c = Converter(
+        unstruct_strat=strat,
+        prefer_attrib_converters=prefer_attrib,
+        detailed_validation=detailed_validation,
+        dict_factory=dict_factory,
+        omit_if_default=omit_if_default,
+    )
+
+    copy = c.copy()
+
+    assert c is not copy
+
+    assert c.unstructure(Simple(1)) == copy.unstructure(Simple(1))
+    assert c.detailed_validation == copy.detailed_validation
+    assert c._prefer_attrib_converters == copy._prefer_attrib_converters
+    assert c._dict_factory == copy._dict_factory
+    assert c.omit_if_default == copy.omit_if_default
+
+    another_copy = c.copy(omit_if_default=not omit_if_default)
+    assert c.omit_if_default != another_copy.omit_if_default
+
+
+@given(
+    strat=unstructure_strats,
+    detailed_validation=...,
+    prefer_attrib=...,
+    dict_factory=one_of(just(dict), just(OrderedDict)),
+)
+def test_copy_hooks(
+    converter_cls: Type[BaseConverter],
+    strat: UnstructureStrategy,
+    prefer_attrib: bool,
+    detailed_validation: bool,
+    dict_factory: Callable,
+):
+    """Un/structure hooks are copied over."""
+    c = converter_cls(
+        unstruct_strat=strat,
+        prefer_attrib_converters=prefer_attrib,
+        detailed_validation=detailed_validation,
+        dict_factory=dict_factory,
+    )
+
+    c.register_unstructure_hook(Simple, lambda s: s.a)
+    c.register_structure_hook(Simple, lambda v, t: Simple(v))
+
+    assert c.unstructure(Simple(1)) == 1
+    assert c.structure(1, Simple) == Simple(1)
+
+    copy = c.copy()
+
+    assert c is not copy
+
+    assert c.unstructure(Simple(1)) == copy.unstructure(Simple(1))
+    assert copy.structure(copy.unstructure(Simple(1)), Simple) == Simple(1)
+    assert c.detailed_validation == copy.detailed_validation
+    assert c._prefer_attrib_converters == copy._prefer_attrib_converters
+    assert c._dict_factory == copy._dict_factory
+
+
+@given(prefer_attrib=..., dict_factory=one_of(just(dict), just(OrderedDict)))
+def test_detailed_validation(prefer_attrib: bool, dict_factory: Callable):
+    """Copies with different detailed validation work correctly."""
+    c = Converter(
+        prefer_attrib_converters=prefer_attrib,
+        detailed_validation=True,
+        dict_factory=dict_factory,
+    )
+
+    # So the converter gets generated.
+    c.structure({"a": 1}, Simple)
+
+    copy = c.copy(detailed_validation=False)
+
+    assert c is not copy
+    assert copy.detailed_validation is False
+
+    with raises(ClassValidationError):
+        c.structure({}, Simple)
+
+    with raises(KeyError):
+        copy.structure({}, Simple)
+
+
+@given(
+    prefer_attrib=...,
+    dict_factory=one_of(just(dict), just(OrderedDict)),
+    detailed_validation=...,
+)
+def test_col_overrides(
+    prefer_attrib: bool, dict_factory: Callable, detailed_validation: bool
+):
+    """Copies with different sequence overrides work correctly."""
+    c = Converter(
+        prefer_attrib_converters=prefer_attrib,
+        detailed_validation=detailed_validation,
+        dict_factory=dict_factory,
+        unstruct_collection_overrides={list: tuple},
+    )
+
+    # So the converter gets generated.
+    assert c.unstructure([1, 2, 3]) == (1, 2, 3)
+    # We also stick a manual hook on there so it gets copied too.
+    c.register_unstructure_hook(Simple, lambda s: s.a)
+
+    copy = c.copy(unstruct_collection_overrides={})
+
+    assert c is not copy
+
+    assert c.unstructure([1, 2, 3]) == (1, 2, 3)
+    assert copy.unstructure([1, 2, 3]) == [1, 2, 3]
+
+    assert c.unstructure(Simple(1)) == 1
+    assert copy.unstructure(Simple(1)) == 1
diff --git a/tests/test_factory_hooks.py b/tests/test_factory_hooks.py
index 6175be0..2dc85cb 100644
--- a/tests/test_factory_hooks.py
+++ b/tests/test_factory_hooks.py
@@ -1,8 +1,6 @@
 """Tests for the factory hooks documentation."""
-import pytest
 from attr import define, fields, has
 
-from cattrs import BaseConverter, Converter
 from cattrs.gen import make_dict_structure_fn, make_dict_unstructure_fn, override
 
 
@@ -11,7 +9,6 @@ def to_camel_case(snake_str):
     return components[0] + "".join(x.title() for x in components[1:])
 
 
-@pytest.mark.parametrize("converter_cls", [BaseConverter, Converter])
 def test_snake_to_camel(converter_cls):
     @define
     class Inner:
diff --git a/tests/test_structure.py b/tests/test_structure.py
index b4053c0..f604a2a 100644
--- a/tests/test_structure.py
+++ b/tests/test_structure.py
@@ -129,7 +129,10 @@ def test_structuring_hetero_tuples(list_of_vals_and_types, detailed_validation):
     converter = BaseConverter(detailed_validation=detailed_validation)
     types = tuple(e[1] for e in list_of_vals_and_types)
     vals = [e[0] for e in list_of_vals_and_types]
-    t = Tuple[types]
+    if types:
+        t = Tuple[types]
+    else:
+        t = Tuple
 
     converted = converter.structure(vals, t)
 
@@ -141,13 +144,24 @@ def test_structuring_hetero_tuples(list_of_vals_and_types, detailed_validation):
     for x, y in zip(types, converted):
         assert isinstance(y, x)
 
+    t2 = Tuple[types + (str,)]  # one longer
+    vals2 = vals + [None]  # one longer
+    expected_exception = IterableValidationError if detailed_validation else ValueError
+    with raises(expected_exception):
+        converter.structure(vals, t2)
+    with raises(expected_exception):
+        converter.structure(vals2, t)
+
 
 @given(lists(primitives_and_type))
 def test_stringifying_tuples(list_of_vals_and_types):
     """Stringify all elements of a heterogeneous tuple."""
     converter = BaseConverter()
     vals = [e[0] for e in list_of_vals_and_types]
-    t = Tuple[(str,) * len(list_of_vals_and_types)]
+    if len(list_of_vals_and_types):
+        t = Tuple[(str,) * len(list_of_vals_and_types)]
+    else:
+        t = Tuple
 
     converted = converter.structure(vals, t)
 
diff --git a/tests/test_validation.py b/tests/test_validation.py
index 4066266..5fceab2 100644
--- a/tests/test_validation.py
+++ b/tests/test_validation.py
@@ -52,9 +52,8 @@ def test_external_class_validation():
     with pytest.raises(ClassValidationError) as exc:
         c.structure({"a": 1, "b": "c", "c": "1"}, Test)
 
-    assert repr(exc.value.exceptions[0]) == repr(
-        ValueError("'b' must be in ['a', 'b'] (got 'c')")
-    )
+    assert type(exc.value.exceptions[0]) == ValueError
+    assert str(exc.value.exceptions[0].args[0]) == "'b' must be in ['a', 'b'] (got 'c')"
 
 
 def test_list_validation():