CLI tool
keleshev/schema avatar
keleshev/schema

schema's README claims Python 2.6 to 3.9 while its classifiers claim 3.6 to 3.11

Schema validation just got Pythonic

2,946 stars230 forksPythonMIT

At a glance

What is it?
A single file library for validating Python data structures, where callables are judged on their return value and the entries inside a list schema are alternatives rather than a sequence. Its support claim, its classifiers and its newest release disagree with each other, and pyproject.toml configures only the linter.
Who is it for?
schema fits a codebase that needs a config or form payload checked at the boundary and wants the checked value back, rather than a boolean answer, because validate returns the data as the Use and And calls have transformed it. Two things to check before depending on it.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 107 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 5, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The tested-version list and the packaging classifiers disagree at both ends

The Installation section states that schema is tested with Python 2.6, 2.7, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9 and PyPy, and that it follows semantic versioning.

The classifiers in setup.py list something else: Python 3.6, 3.7, 3.8, 3.9, 3.10 and 3.11, plus the PyPy implementation marker. So the README claims two Python 2 versions the metadata never mentions and stops two minor releases short of what the metadata claims. Neither list is obviously the intended one.

The dependency file adds a third data point. Its only entry is a conditional backport, `contextlib2` for Python below 3.3, which is a dependency for an interpreter the classifiers no longer claim to support.

The version story is looser still. The newest tag is v0.7.6 from 2024-03-26, the one before it v0.7.5 from 2021-12-01, and the one before that v0.7.3 from 2020-07-31, so there is a two year gap and a skipped number in the visible history. The last push to master is 2026-06-20, more than two years after the newest release, and the package still declares its own development status as Alpha.

Inside a list schema the entries are alternatives, not a sequence

The documentation for containers is one sentence: if a schema encounters a list, tuple, set or frozenset, it validates the contents of the data container against all schemas listed inside that container and aggregates all errors.

The two examples show what that means, and it is not what most people expect. Validating the list `[1, 1, 0, 1]` against the schema `[1, 0]` succeeds and returns the input unchanged. Validating the tuple `(5, 7, 8, 'not int or float here')` against `(int, float)` fails, with an error that names the alternatives as an `Or` and then lists both failures:

code
>>> Schema([1, 0]).validate([1, 1, 0, 1])
[1, 1, 0, 1]

>>> Schema((int, float)).validate((5, 7, 8, 'not int or float here'))
Traceback (most recent call last):
...
schema.SchemaError: Or(<class 'int'>, <class 'float'>) did not validate 'not int or float here'
'not int or float here' should be instance of 'int'
'not int or float here' should be instance of 'float'

So the schemas listed inside a container schema are alternatives that each element may satisfy, not a positional pattern that elements must follow in order. A list schema reads as a list of items that are any one of these, and the error aggregates every alternative's failure so you see all of them at once rather than only the first.

A callable validator is judged on its return value, not on whether it raised

Types are the simple case: a schema of `int`, `str` or `object` checks whether the data is an instance of that type and raises otherwise, with a dedicated `SchemaUnexpectedTypeError` naming the type it wanted. Note that `object` accepts anything, since every value is an instance of it.

Callables are the case worth reading closely. If the schema is a function, a class, or an object with a `__call__` method, the library calls it and continues only if the return value evaluates to true, raising `SchemaError` otherwise. The examples are `os.path.exists` and a lambda checking that a number is above zero, and the error messages quote the callable and the offending value:

code
>>> Schema(os.path.exists).validate('./')
'./'

>>> Schema(os.path.exists).validate('./non-existent/')
Traceback (most recent call last):
...
schema.SchemaError: exists('./non-existent/') should evaluate to True

The consequence is that a validator has to return something truthy. One that returns `None` on success, which is the Python default, fails; so does one that returns an empty collection for a legitimate empty value. Raising is not how a callable reports failure either, since the return value is what gets tested.

Use formats its error with the callable's __name__, so a nameless callable fails inside the handler

`Use` is the conversion hook, and its whole implementation is shown:

code
class Use(object):

    def __init__(self, callable_):
        self._callable = callable_

    def validate(self, data):
        try:
            return self._callable(data)
        except Exception as e:
            raise SchemaError('%r raised %r' % (self._callable.__name__, e))

The success path is a plain call, so `Schema(Use(int)).validate('123')` returns the integer 123, and the quick example converts ages written as strings while lowercasing genders, which is why the validated output differs from the input.

The error path formats `self._callable.__name__`. Functions and lambdas have that attribute; a class instance with a `__call__` method, or a partial, does not. In those cases the handler raises `AttributeError` while building the message, so the caller sees an attribute error rather than the `SchemaError` the library promises.

Two other things are visible here. Validation is not free of side effects: the documentation's own example wraps `open` in append mode and validates a filename, and the validated value is the open file object. And `Const` is the counterpart for when you want to check a transformed copy but keep the original data, which the documentation demonstrates by nesting a `Const` around a validating conversion inside an `And`.

The install advice offers a single file, while the repository ships a package directory

Two installation paths are given. `pip install schema`, or easy_install. Then the alternative: you can just drop the `schema.py` file into your project, because it is self-contained.

The repository is not laid out that way. The tree holds a `schema/` package directory, and setup.py declares `packages=["schema"]`, which is what gets installed. The version number is not in setup.py at all: it is read out of `schema/__init__.py` with a regular expression looking for an `__version__` assignment, and if the line is missing the script prints a message and exits with status 1 rather than installing something unversioned.

So the self-contained single-file description describes a shape the current repository does not have. Vendoring remains a reasonable thing to want for a library this small, but it means choosing a commit and owning the update, and the pip route is what the packaging metadata actually supports.

The long description is read from `README.rst` and declared as `text/x-rst`, which is why the documentation is written in reStructuredText with `.. code::` directives throughout.

pyproject.toml configures the linter only, and its ignore list has a duplicate entry

The pyproject file is four lines of configuration and nothing else. There is no build system table, so the build is still setup.py, and no project metadata, so the metadata is still in setup.py.

What it does contain is a ruff lint section. `extend-select` adds the import sorting rule, and the ignore list is worth reading in full: F403, E501, N802, N803, N806, C901, D100, D102, D102 and D10.

Two things stand out. D102 appears twice, which is harmless but suggests the list accretes rather than being regenerated. And the naming rules N802, N803 and N806 are the ones that let this library's own code exist: they are what allows a method named `validate` and an attribute named `_callable` to pass, which is most of the public surface of `Use`, `Regex` and `Const`. E501 being ignored means long lines are not reported either, which in a README whose value is the line-by-line examples is a reasonable trade and in the source is worth knowing.

The remaining setup detail is `install_requires`, which is the raw contents of requirements.txt split on newlines. The single conditional dependency line passes through with its marker intact, but any blank line in that file would become an empty requirement string.

Travis badges on the README, a generated changelog, and the tests at the repository root

The README is `README.rst`, which is why it opens with an underline of equals signs and uses reStructuredText image and code directives. Its two badges point at Travis CI, with the build status image hosted on `secure.travis-ci.org` and targeting `travis-ci.org`, both filtered to the master branch, and a second badge for codecov. The tree still carries a `.travis.yml`.

The changelog is machine written. A `.gitchangelog.rc` config sits at the root beside a `CHANGELOG.md`, which is the arrangement that generates release notes from commit messages. A `.pre-commit-config.yaml` runs the same checks before a commit reaches that log.

The tests are not in a directory. `test_schema.py` and `test_mypy_use_callable.py` both sit at the repository root, and the second name says exactly what it is: a typing check on the callable that `Use` wraps. Test execution is configured through `tox.ini`, and `.editorconfig` handles whitespace.

Together those files describe a project whose tooling was assembled for Travis and codecov and kept working, which is consistent with a library whose newest release predates its last commit by more than two years.

Editorial conclusion

schema fits a codebase that needs a config or form payload checked at the boundary and wants the checked value back, rather than a boolean answer, because validate returns the data as the Use and And calls have transformed it. Two things to check before depending on it. The support story is inconsistent in the repository itself: the README's tested-version list and the packaging classifiers disagree at both ends, and the newest tagged release is v0.7.6 from 2024-03-26 while the last push was 2026-06-20, so a version number tells you little about what you would install today. And the semantics worth internalising before writing a schema are two: inside a list, tuple or set schema the listed schemas are alternatives, and a callable validator is judged on whether its return value is truthy rather than on whether it raised. Both are demonstrated in the documentation rather than stated as rules.

Frequently asked questions

What is the schema library used for?

Validating Python data structures, such as values that came from config files, forms, external services or command line parsing and were converted from JSON or YAML. `Schema.validate` returns the validated data, optionally converted by `Use` calls, raises `SchemaError` on invalid input, and `is_valid` returns a boolean.

How do I install schema?

`pip install schema`, or easy_install. The README also says you can drop the schema.py file into your project because it is self-contained, though the repository ships a `schema/` package directory and setup.py declares `packages=["schema"]`.

Which Python versions does schema support?

The README says it is tested with Python 2.6, 2.7, 3.2 through 3.9 and PyPy. The classifiers in setup.py list Python 3.6 through 3.11 and PyPy. The only declared dependency is contextlib2 for Python below 3.3.

How do you convert a value while validating it in schema?

Wrap the callable in `Use`, so `Schema(Use(int)).validate('123')` returns the integer 123. Use calls the function and re-raises any exception as a `SchemaError`. To check a converted copy while keeping the original data, nest the conversion inside `Const`, which the documentation shows within an `And`.

Official sources

  1. Issues
  2. keleshev/schema on GitHub
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/keleshev-schema.svg)](https://hysenlabs.com/projects/keleshev-schema)