NEON for Python

The package nette-neon reads and writes NEON in Python, for example your config.neon or the phpstan.neon of a PHP project. With it you:

  • decode a configuration file into Python values and encode values back to NEON
  • edit a file while every comment and space you did not touch stays where it was
  • check files with neon-lint in your editor, in CI or after an AI agent

Installation

pip install nette-neon

Or uv add nette-neon. The package is imported as neon. It requires Python 3.13 or newer, it is fully typed and has no dependencies.

Decoding and Encoding

The function decode() turns NEON into Python values, decode_file() does the same for a file, and encode() turns values back:

from neon import decode, decode_file, encode

decode("roles: [guest, admin]")              # {'roles': ['guest', 'admin']}
config = decode_file("config.neon")
encode({"roles": ["guest", "admin"]})        # '{roles: [guest, admin]}'
encode({"roles": ["guest", "admin"]}, True)  # 'roles:\n\t- guest\n\t- admin\n\n'

Instead of True, the second argument of encode() can be a function that decides for each array whether to write it as a block. It gets the array and the list of keys on the way to it:

encode(config, lambda value, path: not path)  # blocks for the first level only

The values of the format become these Python values:

NEON Python
null, true, yes … None, bool
integer int, of any size
float float
string str
date neon.Date
entity, chain neon.Entity
sequence list
mapping dict with string keys, in the order of the file

The mapping is one-to-one: 1.0 stays a float, a large integer stays exact, {} is a dict and [] a list. A neon.Date holds the date as it was written, including a missing time zone; to_datetime() turns it into a datetime. When encoding, datetime and date are accepted as well.

Errors

When the input is not valid NEON, decode() and parse() raise neon.NeonError. Its message says where the problem is, and its attribute position holds the line and the column:

from neon import NeonError, decode

try:
    decode("database:\n\thost: localhost\n\thost: db")
except NeonError as e:
    print(e)                # Duplicated key 'host' on line 3 at column 2
    print(e.position.line)  # 3

Editing Files

The function parse() gives the syntax tree of a file, and through it you change the file so that the rest of it stays as its author wrote it:

from pathlib import Path
from neon import parse

file = Path("config.neon")
doc = parse(file.read_text(encoding="utf-8"))
doc.set_value(["database", "host"], "db.example.com")
file.write_text(doc.print(), encoding="utf-8")

How to change, add, remove and rename items, and how to find your way around the tree, is described on the page Editing Files.

Checking Files: neon-lint

The package comes with the command neon-lint. Give it files or directories, and it prints one line for each problem:

neon-lint config/
config/local.neon:3:2: Duplicated key 'user'

The command, its options and its exit codes are the same as in the PHP library. To run it without installing the package, use uvx --from nette-neon neon-lint config/.

An AI agent that runs it after every change of a .neon file learns about a broken file right away. The guide for AI agents shows how to set it up.

More examples, each with its real output, are in the repository of the library.