Skip to main content

litfix

Repair and parse Python/JSON-like literal strings that contain raw, unescaped control characters embedded inside quoted string values.

The problem

>>> import ast
>>> data = "{'title': 'Azur\r\nLane', 'new': True}"
>>> ast.literal_eval(data)
SyntaxError: unterminated string literal (detected at line 1)

This happens when upstream code builds a dict/list containing strings that already have real \r/\n bytes in them (e.g. scraped HTML, multi-line titles), then serializes it with str(obj) instead of repr(obj) or json.dumps(obj). The result looks like a normal Python-repr or JSON string, but has literal control characters sitting inside the quotes -- which is invalid syntax for ast.literal_eval, json.loads, and lenient parsers like json5 alike, since none of them allow a bare newline inside a quoted string.

The fix

litfix walks the source character by character, tracks whether the cursor is inside a quoted string (honoring backslash escapes), and re-escapes control characters only when they're inside quotes. Whitespace between tokens (e.g. pretty-printed line wrapping) is left untouched, since it's already valid syntax there.

>>> from litfix import parse
>>> parse("{'title': 'Azur\r\nLane', 'new': True}")
{'title': 'Azur\r\nLane', 'new': True}

Install

pip install litfix          # once published
pip install -e ".[dev]"     # local editable install with test deps

Zero runtime dependencies.

API

from litfix import (
    parse,                # auto-detect: Python literal, then JSON
    robust_literal_eval,   # ast.literal_eval with auto-repair
    robust_json_loads,     # json.loads with auto-repair
    sanitize_literal,      # just the repair step, for custom pipelines
    RepairReport,          # diagnostics: what got fixed, and where
    LiteralRepairError,    # raised if repair still can't make it parse
)

parse(s, *, verbose=False)

Best-effort parse: tries Python-literal syntax first (single quotes, True/False/None), falls back to JSON. This is the one you want if you don't know or don't care which flavor the source uses.

parse("[{'a': 1, 'b': True}]")        # -> [{'a': 1, 'b': True}]
parse('{"a": 1, "b": true}')          # -> {'a': 1, 'b': True}

robust_literal_eval(s, *, verbose=False) / robust_json_loads(s, *, verbose=False)

Same repair strategy, pinned to one syntax. Pass verbose=True to get back (result, RepairReport) instead of just result:

result, report = robust_literal_eval(raw, verbose=True)
if report.was_modified:
    print(report)   # "repaired 2 control char(s) inside string literals ('\r'x1, '\n'x1)"

sanitize_literal(s, *, report=None)

The raw repair pass, if you want to wire it into your own pipeline before doing something other than literal_eval/json.loads with it.

Errors

If the input still can't be parsed after sanitization, litfix raises LiteralRepairError (a ValueError) with .original_error (the underlying SyntaxError/JSONDecodeError) and .source (the sanitized text that was attempted), so you can see exactly what was tried.

CLI

litfix dump.txt                       # parse -> pretty JSON on stdout
litfix dump.txt -o clean.json         # write to a file instead
cat dump.txt | litfix -v              # read stdin, print repair diagnostics to stderr
litfix dump.txt --mode literal        # force Python-literal parsing
litfix dump.txt --indent 0            # compact single-line JSON output

Exit code is 0 on success, 1 on unrecoverable parse failure (with an error message on stderr).

Testing

pip install -e ".[dev]"
pytest

License

MIT

👤 Author

Hadi Cahyadi

Buy Me a Coffee

Donate via Ko-fi

Support me on Patreon

Metadata

Release files for litfix 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for litfix 1.0.0
File Size Uploaded
litfix-1.0.0.tar.gz 10.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for litfix 1.0.0
File Interpreter ABI Platform
litfix-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 20.4 kB

Release files / litfix-1.0.0.tar.gz

Download URL litfix-1.0.0.tar.gz
Size 10.6 kB
Tags Source
SHA-256 checksum
How to use checksums
e2c14788209b1a9efe5f2df6c9d9dfff315410a88bea5fbb4cab6fa72090161b
BLAKE2b-256 checksum
How to use checksums
917bc8b14282b5aed9b128ffb3aef077c95e234c51a398970b09361ff3c258bb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/4.0.2 CPython/3.11.4

Release files / litfix-1.0.0-py3-none-any.whl

Download URL litfix-1.0.0-py3-none-any.whl
Size 9.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
95c2b27a9bf7129dcc116de2601501c9930209cad54be7d80669dcee0f1c8d12
BLAKE2b-256 checksum
How to use checksums
39c5a9cd5e1a1ac69608dc8a9f21f79218746298533bb20c4a65bbb1573e71a4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/4.0.2 CPython/3.11.4

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page