diff --git a/docs/source/news.rst b/docs/source/news.rst
index bb2ba9b..b0409df 100644
--- a/docs/source/news.rst
+++ b/docs/source/news.rst
@@ -13,6 +13,7 @@ Features:
 * NumPy integers, floats, bools and arrays are now automatically serialized to JSON, via a new default JSON encoder (``eliot.json.EliotJSONEncoder``).
 * Dask support: replace ``dask.compute()`` with ``eliot.dask.compute_with_trace()`` to automatically preserve Eliot context for ``Bag`` and ``Delayed`` Dask computations. See :ref:`dask_usage` for details.
 * New decorator, ``@eliot.log_call``, which automatically creates an action that starts when function is called and ends when it returns. See :ref:`log_call decorator`.
+* A parser library for parsing serialized Eliot JSON messages into a tree of Python objects. See :ref:`parsing_logs` for details.
 
 Testing features:
 
diff --git a/docs/source/reading/reading.rst b/docs/source/reading/reading.rst
index 1312135..89a0ab9 100644
--- a/docs/source/reading/reading.rst
+++ b/docs/source/reading/reading.rst
@@ -41,3 +41,24 @@ For example, here's how you'd extract a particular field with `jq`_:
 
 .. _eliot-tree: https://github.com/jonathanj/eliottree
 .. _jq: https://stedolan.github.io/jq/
+
+
+.. _parsing_logs:
+
+Parsing Logs
+------------
+
+Eliot also includes a parser for parsing logs into Python objects:
+
+.. code-block:: python
+
+   import json
+   from eliot.parse import Parser
+
+   def load_messages(logfile_path):
+      for line in open(logfile_path):
+          yield json.loads(line)
+       
+   def parse(logfile_path):
+       for task in Parser.parse_stream(load_messages(logfile_path)):
+           print("Root action type is", task.root().action_type)
diff --git a/eliot/__init__.py b/eliot/__init__.py
index 2d998fc..d21ecbb 100644
--- a/eliot/__init__.py
+++ b/eliot/__init__.py
@@ -47,6 +47,18 @@ add_destinations = Logger._destinations.add
 remove_destination = removeDestination
 add_global_fields = addGlobalFields
 
+# Backwards compatibility for old versions of eliot-tree, which rely on
+# eliot._parse:
+def _parse_compat():
+    # Force eliot.parse to be imported in way that works with old Python:
+    from .parse import Parser
+    del Parser
+    import sys
+    sys.modules["eliot._parse"] = sys.modules["eliot.parse"]
+    return sys.modules["eliot.parse"]
+_parse = _parse_compat()
+del _parse_compat
+
 
 __all__ = [
     "Message",
@@ -83,7 +95,11 @@ __all__ = [
     "to_file",
     "log_call",
     "__version__",
+
+    # Backwards compat for eliot-tree:
+    "_parse",
 ]
 
+
 __version__ = get_versions()['version']
 del get_versions
diff --git a/eliot/_parse.py b/eliot/parse.py
similarity index 98%
rename from eliot/_parse.py
rename to eliot/parse.py
index 91474d0..bca580c 100644
--- a/eliot/_parse.py
+++ b/eliot/parse.py
@@ -181,3 +181,6 @@ class Parser(PClass):
                 yield task
         for task in parser.incomplete_tasks():
             yield task
+
+
+__all__ = ["Parser", "Task", "TaskLevel", "WrittenMessage", "WrittenAction"]

Make eliot._parse public so eliot-tree isn't depending on private API
`eliot-tree` depends on `eliot._parse`, which is private. Really, it's a stable API, and it should be useful to others as well.

So:

1. Make it public.
2. Add backwards compat `eliot._parse` for old eliot-tree installs.
3. Document it, at least a little.

CC @jonathanj 
diff --git a/eliot/tests/corotests.py b/eliot/tests/corotests.py
index de844c8..78e3501 100644
--- a/eliot/tests/corotests.py
+++ b/eliot/tests/corotests.py
@@ -10,7 +10,7 @@ from threading import Thread
 from unittest import TestCase
 
 from ..testing import capture_logging
-from .._parse import Parser
+from ..parse import Parser
 from .. import start_action
 from .._action import _ExecutionContext, _context, use_asyncio_context
 from .._asyncio import AsyncioContext
diff --git a/eliot/tests/test_action.py b/eliot/tests/test_action.py
index 86eb5e1..85b91ed 100644
--- a/eliot/tests/test_action.py
+++ b/eliot/tests/test_action.py
@@ -44,7 +44,7 @@ from .._message import (
 from .._output import MemoryLogger
 from .._validation import ActionType, Field, _ActionSerializers
 from ..testing import assertContainsFields, capture_logging
-from .._parse import Parser
+from ..parse import Parser
 from .. import (
     _action,
     add_destination,
diff --git a/eliot/tests/test_parse.py b/eliot/tests/test_parse.py
index a24ad00..77410ee 100644
--- a/eliot/tests/test_parse.py
+++ b/eliot/tests/test_parse.py
@@ -16,7 +16,7 @@ from pyrsistent import PClass, field, pvector_field
 
 from .. import start_action, Message
 from ..testing import MemoryLogger
-from .._parse import Task, Parser
+from ..parse import Task, Parser
 from .._message import (
     WrittenMessage,
     MESSAGE_TYPE_FIELD,
@@ -295,3 +295,15 @@ class ParserTests(TestCase):
                 if m is not None]))
         assertCountEqual(
             self, all_tasks, [parse_to_task(msgs) for msgs in all_messages])
+
+
+class BackwardsCompatibility(TestCase):
+    """Tests for backwards compatibility."""
+
+    def test_imports(self):
+        """Old ways of importing still work."""
+        import eliot._parse
+        from eliot import _parse
+        import eliot.parse
+        self.assertIs(eliot.parse, eliot._parse)
+        self.assertIs(_parse, eliot.parse)

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-cov pytest-xdist pytest-mock pytest-asyncio
apt-get update apt-get install -y daemontools
3.7
requirements/base.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.13
attrs==24.2.0
Babel==2.14.0
bleach==6.0.0
boltons==25.0.0
certifi @ file:///croot/certifi_1671487769961/work/certifi
cffi==1.15.1
charset-normalizer==3.4.1
coverage==7.2.7
cryptography==44.0.2
docutils==0.17.1
-e git+https://github.com/itamarst/eliot.git@97cc338d97b26da5c79e06f795f9e933a1b63d4a#egg=eliot
exceptiongroup==1.2.2
execnet==2.0.2
flake8==5.0.4
hypothesis==6.79.4
idna==3.10
imagesize==1.4.1
importlib-metadata==4.2.0
iniconfig==2.0.0
jaraco.classes==3.2.3
jeepney==0.9.0
Jinja2==3.1.6
keyring==23.9.3
markdown-it-py==2.2.0
MarkupSafe==2.1.5
mccabe==0.7.0
mdurl==0.1.2
more-itertools==9.1.0
packaging==24.0
pkginfo==1.10.0
platformdirs==4.0.0
pluggy==1.2.0
pycodestyle==2.9.1
pycparser==2.21
pyflakes==2.5.0
Pygments==2.17.2
pyrsistent==0.19.3
pytest==7.4.4
pytest-asyncio==0.21.2
pytest-cov==4.1.0
pytest-mock==3.11.1
pytest-xdist==3.5.0
pytz==2025.2
readme-renderer==37.3
requests==2.31.0
requests-toolbelt==1.0.0
rfc3986==2.0.0
rich==13.8.1
SecretStorage==3.3.3
six==1.17.0
snowballstemmer==2.2.0
sortedcontainers==2.4.0
Sphinx==4.3.2
sphinx-rtd-theme==1.3.0
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.0
sphinxcontrib-jquery==4.1
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
testtools==2.7.1
tomli==2.0.1
twine==4.0.2
typing_extensions==4.7.1
urllib3==2.0.7
webencodings==0.5.1
yapf==0.43.0
zipp==3.15.0
zope.interface==6.4.post2

name: eliot
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
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.13
      - attrs==24.2.0
      - babel==2.14.0
      - bleach==6.0.0
      - boltons==25.0.0
      - cffi==1.15.1
      - charset-normalizer==3.4.1
      - coverage==7.2.7
      - cryptography==44.0.2
      - docutils==0.17.1
      - exceptiongroup==1.2.2
      - execnet==2.0.2
      - flake8==5.0.4
      - hypothesis==6.79.4
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==4.2.0
      - iniconfig==2.0.0
      - jaraco-classes==3.2.3
      - jeepney==0.9.0
      - jinja2==3.1.6
      - keyring==23.9.3
      - markdown-it-py==2.2.0
      - markupsafe==2.1.5
      - mccabe==0.7.0
      - mdurl==0.1.2
      - more-itertools==9.1.0
      - packaging==24.0
      - pkginfo==1.10.0
      - platformdirs==4.0.0
      - pluggy==1.2.0
      - pycodestyle==2.9.1
      - pycparser==2.21
      - pyflakes==2.5.0
      - pygments==2.17.2
      - pyrsistent==0.19.3
      - pytest==7.4.4
      - pytest-asyncio==0.21.2
      - pytest-cov==4.1.0
      - pytest-mock==3.11.1
      - pytest-xdist==3.5.0
      - pytz==2025.2
      - readme-renderer==37.3
      - requests==2.31.0
      - requests-toolbelt==1.0.0
      - rfc3986==2.0.0
      - rich==13.8.1
      - secretstorage==3.3.3
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sortedcontainers==2.4.0
      - sphinx==4.3.2
      - sphinx-rtd-theme==1.3.0
      - sphinxcontrib-applehelp==1.0.2
      - sphinxcontrib-devhelp==1.0.2
      - sphinxcontrib-htmlhelp==2.0.0
      - sphinxcontrib-jquery==4.1
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==1.0.3
      - sphinxcontrib-serializinghtml==1.1.5
      - testtools==2.7.1
      - tomli==2.0.1
      - twine==4.0.2
      - typing-extensions==4.7.1
      - urllib3==2.0.7
      - webencodings==0.5.1
      - yapf==0.43.0
      - zipp==3.15.0
      - zope-interface==6.4.post2
prefix: /opt/conda/envs/eliot

eliot/tests/corotests.py::CoroutineTests::test_await_inherits_coroutine_contexts eliot/tests/corotests.py::CoroutineTests::test_coroutine_vs_main_thread_context eliot/tests/corotests.py::CoroutineTests::test_multiple_coroutines_contexts eliot/tests/corotests.py::ContextTests::test_await_inherits_coroutine_context eliot/tests/corotests.py::ContextTests::test_coroutine_vs_main_thread_context eliot/tests/corotests.py::ContextTests::test_multiple_coroutines_contexts eliot/tests/corotests.py::ContextTests::test_threadSafety eliot/tests/test_action.py::ExecutionContextTests::test_globalInstance eliot/tests/test_action.py::ExecutionContextTests::test_multipleCurrent eliot/tests/test_action.py::ExecutionContextTests::test_nothingPushed eliot/tests/test_action.py::ExecutionContextTests::test_popMultiple eliot/tests/test_action.py::ExecutionContextTests::test_popSingle eliot/tests/test_action.py::ExecutionContextTests::test_pushMultiple eliot/tests/test_action.py::ExecutionContextTests::test_pushSingle eliot/tests/test_action.py::ExecutionContextTests::test_threadSafety eliot/tests/test_action.py::ExecutionContextTests::test_threadStart eliot/tests/test_action.py::ActionTests::test_addSuccessFields eliot/tests/test_action.py::ActionTests::test_child eliot/tests/test_action.py::ActionTests::test_childLevel eliot/tests/test_action.py::ActionTests::test_childSerializers eliot/tests/test_action.py::ActionTests::test_failureFinishSerializer eliot/tests/test_action.py::ActionTests::test_finish eliot/tests/test_action.py::ActionTests::test_finishWithBadException eliot/tests/test_action.py::ActionTests::test_multipleFinishCalls eliot/tests/test_action.py::ActionTests::test_nextTaskLevel eliot/tests/test_action.py::ActionTests::test_run eliot/tests/test_action.py::ActionTests::test_runContext eliot/tests/test_action.py::ActionTests::test_runContextUnsetOnRaise eliot/tests/test_action.py::ActionTests::test_runContextUnsetOnReturn eliot/tests/test_action.py::ActionTests::test_start eliot/tests/test_action.py::ActionTests::test_startFieldsNotInFinish eliot/tests/test_action.py::ActionTests::test_startMessageSerialization eliot/tests/test_action.py::ActionTests::test_stringActionCompatibility eliot/tests/test_action.py::ActionTests::test_stringActionCompatibilityWarning eliot/tests/test_action.py::ActionTests::test_successfulFinishSerializer eliot/tests/test_action.py::ActionTests::test_task_uuid eliot/tests/test_action.py::ActionTests::test_withContextNoLogging eliot/tests/test_action.py::ActionTests::test_withContextReturnsaction eliot/tests/test_action.py::ActionTests::test_withContextSetsContext eliot/tests/test_action.py::ActionTests::test_withContextUnsetOnRaise eliot/tests/test_action.py::ActionTests::test_withContextUnsetOnReturn eliot/tests/test_action.py::ActionTests::test_withLogsExceptionMessage eliot/tests/test_action.py::ActionTests::test_withLogsSuccessfulFinishMessage eliot/tests/test_action.py::ActionTests::test_withReturnValue eliot/tests/test_action.py::ActionTests::test_withSetsContext eliot/tests/test_action.py::ActionTests::test_withUnsetOnRaise eliot/tests/test_action.py::ActionTests::test_withUnsetOnReturn eliot/tests/test_action.py::StartActionAndTaskTests::test_startActionNoLogger eliot/tests/test_action.py::StartActionAndTaskTests::test_startActionNoParent eliot/tests/test_action.py::StartActionAndTaskTests::test_startActionNoParentLogStart eliot/tests/test_action.py::StartActionAndTaskTests::test_startActionSerializers eliot/tests/test_action.py::StartActionAndTaskTests::test_startActionWithParent eliot/tests/test_action.py::StartActionAndTaskTests::test_startActionWithParentLogStart eliot/tests/test_action.py::StartActionAndTaskTests::test_startTaskLogsStart eliot/tests/test_action.py::StartActionAndTaskTests::test_startTaskNewAction eliot/tests/test_action.py::StartActionAndTaskTests::test_startTaskNewUUID eliot/tests/test_action.py::StartActionAndTaskTests::test_startTaskNoLogger eliot/tests/test_action.py::StartActionAndTaskTests::test_startTaskSerializers eliot/tests/test_action.py::StartActionAndTaskTests::test_start_action_default_action_type eliot/tests/test_action.py::StartActionAndTaskTests::test_start_task_default_action_type eliot/tests/test_action.py::PEP8Tests::test_add_success_fields eliot/tests/test_action.py::PEP8Tests::test_continue_task eliot/tests/test_action.py::PEP8Tests::test_serialize_task_id eliot/tests/test_action.py::SerializationTests::test_continueTaskNoLogger eliot/tests/test_action.py::SerializationTests::test_continueTaskRequiredTaskId eliot/tests/test_action.py::SerializationTests::test_continueTaskReturnsAction eliot/tests/test_action.py::SerializationTests::test_continueTaskStartsAction eliot/tests/test_action.py::SerializationTests::test_continueTaskUnicode eliot/tests/test_action.py::SerializationTests::test_serializeTaskId eliot/tests/test_action.py::TaskLevelTests::test_child_greater_than_parent eliot/tests/test_action.py::TaskLevelTests::test_fromString eliot/tests/test_action.py::TaskLevelTests::test_from_string eliot/tests/test_action.py::TaskLevelTests::test_next_sibling eliot/tests/test_action.py::TaskLevelTests::test_next_sibling_greater eliot/tests/test_action.py::TaskLevelTests::test_parent_of_child eliot/tests/test_action.py::TaskLevelTests::test_parent_of_root eliot/tests/test_action.py::TaskLevelTests::test_toString eliot/tests/test_action.py::TaskLevelTests::test_to_string eliot/tests/test_action.py::WrittenActionTests::test_action_type_mismatch eliot/tests/test_action.py::WrittenActionTests::test_children eliot/tests/test_action.py::WrittenActionTests::test_different_task_uuid eliot/tests/test_action.py::WrittenActionTests::test_duplicate_task_level eliot/tests/test_action.py::WrittenActionTests::test_end_has_no_status eliot/tests/test_action.py::WrittenActionTests::test_failed_end eliot/tests/test_action.py::WrittenActionTests::test_from_single_child_message eliot/tests/test_action.py::WrittenActionTests::test_from_single_end_message eliot/tests/test_action.py::WrittenActionTests::test_from_single_start_message eliot/tests/test_action.py::WrittenActionTests::test_invalid_start_message_missing_status eliot/tests/test_action.py::WrittenActionTests::test_invalid_start_message_wrong_status eliot/tests/test_action.py::WrittenActionTests::test_invalid_task_level_in_start_message eliot/tests/test_action.py::WrittenActionTests::test_successful_end eliot/tests/test_action.py::WrittenActionTests::test_wrong_task_level eliot/tests/test_action.py::WrittenActionTests::test_wrong_task_uuid eliot/tests/test_action.py::FailedActionExtractionTests::test_environmenterror eliot/tests/test_action.py::FailedActionExtractionTests::test_error_in_extracter eliot/tests/test_action.py::FailedActionExtractionTests::test_matching_class eliot/tests/test_action.py::FailedActionExtractionTests::test_regular_fields eliot/tests/test_action.py::FailedActionExtractionTests::test_subclass_falls_back_to_parent eliot/tests/test_action.py::FailedActionExtractionTests::test_subclass_matches_first eliot/tests/test_action.py::PreserveContextTests::test_callable_only_once eliot/tests/test_action.py::PreserveContextTests::test_no_context eliot/tests/test_action.py::PreserveContextTests::test_with_context_calls_underlying eliot/tests/test_action.py::PreserveContextTests::test_with_context_preserves_context eliot/tests/test_action.py::LogCallTests::test_action_type eliot/tests/test_action.py::LogCallTests::test_default_argument_given eliot/tests/test_action.py::LogCallTests::test_default_argument_missing eliot/tests/test_action.py::LogCallTests::test_exception eliot/tests/test_action.py::LogCallTests::test_no_args_return eliot/tests/test_action.py::LogCallTests::test_no_result eliot/tests/test_action.py::LogCallTests::test_pickleable eliot/tests/test_action.py::LogCallTests::test_star_args_kwargs eliot/tests/test_action.py::LogCallTests::test_whitelist_args eliot/tests/test_parse.py::TaskTests::test_is_complete eliot/tests/test_parse.py::TaskTests::test_missing_action eliot/tests/test_parse.py::TaskTests::test_parse_contents eliot/tests/test_parse.py::TaskTests::test_parse_from_random_order eliot/tests/test_parse.py::ParserTests::test_incomplete_tasks eliot/tests/test_parse.py::ParserTests::test_parse_into_tasks eliot/tests/test_parse.py::ParserTests::test_parse_stream eliot/tests/test_parse.py::BackwardsCompatibility::test_imports
docs/source/news.rst docs/source/reading/reading.rst eliot/_parse.py eliot/__init__.py
docs/source/news.rst docs/source/reading/reading.rst eliot/parse.py eliot/__init__.py