codecov[bot]: # [Codecov](https://codecov.io/gh/python-trio/outcome/pull/11?src=pr&el=h1) Report
> Merging [#11](https://codecov.io/gh/python-trio/outcome/pull/11?src=pr&el=desc) into [master](https://codecov.io/gh/python-trio/outcome/commit/9df51224a3e0efa3fffa0e950df1d4e680fa9c3e?src=pr&el=desc) will **decrease** coverage by `3.37%`.
> The diff coverage is `85.96%`.

[![Impacted file tree graph](https://codecov.io/gh/python-trio/outcome/pull/11/graphs/tree.svg?token=TT94ZGhpkk&src=pr&width=650&height=150)](https://codecov.io/gh/python-trio/outcome/pull/11?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master      #11      +/-   ##
==========================================
- Coverage     100%   96.62%   -3.38%     
==========================================
  Files           8        8              
  Lines         196      237      +41     
  Branches       10       16       +6     
==========================================
+ Hits          196      229      +33     
- Misses          0        4       +4     
- Partials        0        4       +4
```


| [Impacted Files](https://codecov.io/gh/python-trio/outcome/pull/11?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [tests/test\_sync.py](https://codecov.io/gh/python-trio/outcome/pull/11/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9zeW5jLnB5) | `100% <100%> (ø)` | :arrow_up: |
| [src/outcome/\_util.py](https://codecov.io/gh/python-trio/outcome/pull/11/diff?src=pr&el=tree#diff-c3JjL291dGNvbWUvX3V0aWwucHk=) | `100% <100%> (ø)` | :arrow_up: |
| [src/outcome/\_\_init\_\_.py](https://codecov.io/gh/python-trio/outcome/pull/11/diff?src=pr&el=tree#diff-c3JjL291dGNvbWUvX19pbml0X18ucHk=) | `100% <100%> (ø)` | :arrow_up: |
| [src/outcome/\_async.py](https://codecov.io/gh/python-trio/outcome/pull/11/diff?src=pr&el=tree#diff-c3JjL291dGNvbWUvX2FzeW5jLnB5) | `86.66% <55.55%> (-13.34%)` | :arrow_down: |
| [src/outcome/\_sync.py](https://codecov.io/gh/python-trio/outcome/pull/11/diff?src=pr&el=tree#diff-c3JjL291dGNvbWUvX3N5bmMucHk=) | `92.85% <88.88%> (-7.15%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/python-trio/outcome/pull/11?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/python-trio/outcome/pull/11?src=pr&el=footer). Last update [9df5122...4b5a233](https://codecov.io/gh/python-trio/outcome/pull/11?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

smurfix: Ah. I didn't know that you could split frozen and not-frozen simply by subclassing.

New commit prepared.
diff --git a/docs/source/api.rst b/docs/source/api.rst
index 0e06fdf..2e7f5cd 100644
--- a/docs/source/api.rst
+++ b/docs/source/api.rst
@@ -21,3 +21,5 @@ API Reference
 .. autoclass:: Error
    :members:
    :inherited-members:
+
+.. autoclass:: AlreadyUsedError
diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst
index bd61a13..220d6d4 100644
--- a/docs/source/tutorial.rst
+++ b/docs/source/tutorial.rst
@@ -25,4 +25,7 @@ which, like before, is the same as::
 
     x = await f(*args, **kwargs)
 
+An Outcome object may not be unwrapped twice. Attempting to do so will
+raise an :class:`AlreadyUsedError`.
+
 See the :ref:`api-reference` for the types involved.
diff --git a/newsfragments/7.feature.rst b/newsfragments/7.feature.rst
new file mode 100644
index 0000000..c6d3dc4
--- /dev/null
+++ b/newsfragments/7.feature.rst
@@ -0,0 +1,3 @@
+An Outcome may only be unwrapped or sent once.
+
+Attempting to do so a second time will raise an :class:`AlreadyUsedError`.
diff --git a/src/outcome/__init__.py b/src/outcome/__init__.py
index a90c3e9..4fe8b0e 100644
--- a/src/outcome/__init__.py
+++ b/src/outcome/__init__.py
@@ -8,12 +8,12 @@ import sys
 
 if sys.version_info >= (3, 5):
     from ._async import Error, Outcome, Value, acapture, capture
-    __all__ = ('Error', 'Outcome', 'Value', 'acapture', 'capture')
+    __all__ = ('Error', 'Outcome', 'Value', 'acapture', 'capture', 'AlreadyUsedError')
 else:
     from ._sync import Error, Outcome, Value, capture
-    __all__ = ('Error', 'Outcome', 'Value', 'capture')
+    __all__ = ('Error', 'Outcome', 'Value', 'capture', 'AlreadyUsedError')
 
 
-from ._util import fixup_module_metadata
+from ._util import fixup_module_metadata, AlreadyUsedError
 fixup_module_metadata(__name__, globals())
 del fixup_module_metadata
diff --git a/src/outcome/_async.py b/src/outcome/_async.py
index 293e5c0..a9ec1ba 100644
--- a/src/outcome/_async.py
+++ b/src/outcome/_async.py
@@ -4,6 +4,7 @@ from ._sync import (
     Error as ErrorBase, Outcome as OutcomeBase, Value as ValueBase
 )
 
+from ._util import AlreadyUsedError
 __all__ = ['Error', 'Outcome', 'Value', 'acapture', 'capture']
 
 
@@ -49,11 +50,13 @@ class Outcome(OutcomeBase):
 
 class Value(ValueBase):
     async def asend(self, agen):
+        self._set_unwrapped()
         return await agen.asend(self.value)
 
 
 class Error(ErrorBase):
     async def asend(self, agen):
+        self._set_unwrapped()
         return await agen.athrow(self.error)
 
 
diff --git a/src/outcome/_sync.py b/src/outcome/_sync.py
index a74bbc3..02a1c09 100644
--- a/src/outcome/_sync.py
+++ b/src/outcome/_sync.py
@@ -4,7 +4,7 @@ from __future__ import absolute_import, division, print_function
 import abc
 import attr
 
-from ._util import ABC
+from ._util import ABC, AlreadyUsedError
 
 __all__ = ['Error', 'Outcome', 'Value', 'capture']
 
@@ -21,7 +21,7 @@ def capture(sync_fn, *args, **kwargs):
     except BaseException as exc:
         return Error(exc)
 
-
+@attr.s(repr=False, init=False, slots=True)
 class Outcome(ABC):
     """An abstract class representing the result of a Python computation.
 
@@ -37,7 +37,13 @@ class Outcome(ABC):
     hashable.
 
     """
-    __slots__ = ()
+    _unwrapped = attr.ib(default=False, cmp=False, init=False)
+
+    def _set_unwrapped(self):
+        if self._unwrapped:
+            raise AlreadyUsedError
+        object.__setattr__(self, '_unwrapped', True)
+
 
     @abc.abstractmethod
     def unwrap(self):
@@ -62,7 +68,7 @@ class Outcome(ABC):
         """
 
 
-@attr.s(frozen=True, repr=False)
+@attr.s(frozen=True, repr=False, slots=True)
 class Value(Outcome):
     """Concrete :class:`Outcome` subclass representing a regular value.
 
@@ -75,13 +81,15 @@ class Value(Outcome):
         return 'Value({!r})'.format(self.value)
 
     def unwrap(self):
+        self._set_unwrapped()
         return self.value
 
     def send(self, gen):
+        self._set_unwrapped()
         return gen.send(self.value)
 
 
-@attr.s(frozen=True, repr=False)
+@attr.s(frozen=True, repr=False, slots=True)
 class Error(Outcome):
     """Concrete :class:`Outcome` subclass representing a raised exception.
 
@@ -94,7 +102,10 @@ class Error(Outcome):
         return 'Error({!r})'.format(self.error)
 
     def unwrap(self):
+        self._set_unwrapped()
         raise self.error
 
     def send(self, it):
+        self._set_unwrapped()
         return it.throw(self.error)
+
diff --git a/src/outcome/_util.py b/src/outcome/_util.py
index a9ca4f5..ec7ce3d 100644
--- a/src/outcome/_util.py
+++ b/src/outcome/_util.py
@@ -6,6 +6,11 @@ import abc
 import sys
 
 
+class AlreadyUsedError(RuntimeError):
+    """An Outcome may not be unwrapped twice."""
+    pass
+
+
 def fixup_module_metadata(module_name, namespace):
     def fix_one(obj):
         mod = getattr(obj, "__module__", None)

Should Outcome objects consume themselves when unwrapped?
From python-trio/trio#466:

@njsmith:
> It's generally a mistake to unwrap the same `Result` object twice, because if it's an exception you'll end up with a corrupted traceback as the same exception object gets raised in two unrelated call stacks.
>
> We should at least document this, and maybe we should make `unwrap` "consume" the object so that if you call it twice then the second time it raises an error.
diff --git a/tests/test_async.py b/tests/test_async.py
index 9dba615..606aa1c 100644
--- a/tests/test_async.py
+++ b/tests/test_async.py
@@ -5,7 +5,7 @@ import trio
 from async_generator import async_generator, yield_
 
 import outcome
-from outcome import Error, Value
+from outcome import Error, Value, AlreadyUsedError
 
 pytestmark = pytest.mark.trio
 
@@ -38,8 +38,15 @@ async def test_asend():
     my_agen = my_agen_func().__aiter__()
     if sys.version_info < (3, 5, 2):
         my_agen = await my_agen
+    v = Value("value")
+    e = Error(KeyError())
     assert (await my_agen.asend(None)) == 1
-    assert (await Value("value").asend(my_agen)) == 2
-    assert (await Error(KeyError()).asend(my_agen)) == 3
+    assert (await v.asend(my_agen)) == 2
+    with pytest.raises(AlreadyUsedError):
+        await v.asend(my_agen)
+
+    assert (await e.asend(my_agen)) == 3
+    with pytest.raises(AlreadyUsedError):
+        await e.asend(my_agen)
     with pytest.raises(StopAsyncIteration):
         await my_agen.asend(None)
diff --git a/tests/test_sync.py b/tests/test_sync.py
index 73900bb..192ac21 100644
--- a/tests/test_sync.py
+++ b/tests/test_sync.py
@@ -5,7 +5,7 @@ import sys
 import pytest
 
 import outcome
-from outcome import Error, Value
+from outcome import Error, Value, AlreadyUsedError
 
 
 def test_Outcome():
@@ -14,13 +14,21 @@ def test_Outcome():
     assert v.unwrap() == 1
     assert repr(v) == "Value(1)"
 
+    with pytest.raises(AlreadyUsedError):
+        v.unwrap()
+
+    v = Value(1)
+
     exc = RuntimeError("oops")
     e = Error(exc)
     assert e.error is exc
     with pytest.raises(RuntimeError):
         e.unwrap()
+    with pytest.raises(AlreadyUsedError):
+        e.unwrap()
     assert repr(e) == "Error({!r})".format(exc)
 
+    e = Error(exc)
     with pytest.raises(TypeError):
         Error("hello")
     with pytest.raises(TypeError):
@@ -33,6 +41,8 @@ def test_Outcome():
     it = iter(expect_1())
     next(it)
     assert v.send(it) == "ok"
+    with pytest.raises(AlreadyUsedError):
+        v.send(it)
 
     def expect_RuntimeError():
         with pytest.raises(RuntimeError):
@@ -42,6 +52,8 @@ def test_Outcome():
     it = iter(expect_RuntimeError())
     next(it)
     assert e.send(it) == "ok"
+    with pytest.raises(AlreadyUsedError):
+        e.send(it)
 
 
 def test_Outcome_eq_hash():

pip install -e .[dev]
parse_log_pytest
pytest
pytest pytest-cov trio pytest-trio
apt-get update apt-get install -y gcc
3.6
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
async-generator==1.10
attrs @ file:///opt/conda/conda-bld/attrs_1642510447205/work
certifi==2021.5.30
contextvars==2.4
coverage==6.2
idna==3.10
immutables==0.19
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1631916693255/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
more-itertools @ file:///tmp/build/80754af9/more-itertools_1637733554872/work
-e git+https://github.com/python-trio/outcome.git@9df51224a3e0efa3fffa0e950df1d4e680fa9c3e#egg=outcome
packaging @ file:///tmp/build/80754af9/packaging_1637314298585/work
pluggy @ file:///tmp/build/80754af9/pluggy_1615976315926/work
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pyparsing @ file:///tmp/build/80754af9/pyparsing_1635766073266/work
pytest==6.2.4
pytest-cov==4.0.0
pytest-trio==0.7.0
sniffio==1.2.0
sortedcontainers==2.4.0
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
tomli==1.2.3
trio==0.19.0
typing_extensions @ file:///opt/conda/conda-bld/typing_extensions_1647553014482/work
zipp @ file:///tmp/build/80754af9/zipp_1633618647012/work

name: outcome
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=21.4.0=pyhd3eb1b0_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - importlib-metadata=4.8.1=py36h06a4308_0
  - importlib_metadata=4.8.1=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - more-itertools=8.12.0=pyhd3eb1b0_0
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=21.3=pyhd3eb1b0_0
  - pip=21.2.2=py36h06a4308_0
  - pluggy=0.13.1=py36h06a4308_0
  - py=1.11.0=pyhd3eb1b0_0
  - pyparsing=3.0.4=pyhd3eb1b0_0
  - pytest=6.2.4=py36h06a4308_2
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - typing_extensions=4.1.1=pyh06a4308_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.6.0=pyhd3eb1b0_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - async-generator==1.10
      - contextvars==2.4
      - coverage==6.2
      - idna==3.10
      - immutables==0.19
      - pytest-cov==4.0.0
      - pytest-trio==0.7.0
      - sniffio==1.2.0
      - sortedcontainers==2.4.0
      - tomli==1.2.3
      - trio==0.19.0
prefix: /opt/conda/envs/outcome

tests/test_async.py::test_asend tests/test_sync.py::test_Outcome tests/test_sync.py::test_Outcome_eq_hash tests/test_sync.py::test_Value_compare tests/test_sync.py::test_capture
tests/test_async.py::test_acapture
src/outcome/__init__.py src/outcome/_sync.py docs/source/api.rst docs/source/tutorial.rst src/outcome/_util.py src/outcome/_async.py newsfragments/7.feature.rst
src/outcome/__init__.py src/outcome/_sync.py docs/source/api.rst docs/source/tutorial.rst src/outcome/_util.py src/outcome/_async.py newsfragments/7.feature.rst