Skip to main content

designspace-lint

What a designspace will do when it is built — before you build it.

A designspace can be perfectly valid and still produce a variable font nobody intended. A glyph missing from the master at the end of an axis quietly reverts to the default's shape past that point. An avar2 mapping written at the default location is thrown away, and every other mapping shifts by what it would have moved. Two rules that swap one glyph over a shared region are settled by how their substitutions sort, not by the order you wrote them. A features.fea that differs from the default's pushes the whole build onto another path, where varLib fails somewhere that has nothing to do with the cause. None of this is an error at build time; the font just comes out wrong.

This library reports those cases, and each report names the consequence, not only the discrepancy.

$ designspace-lint Family.designspace
error 4.11 /eacute Weight 400 of 900: missing at Weight maximum (900); reverts to the default shape past 400
error 1.18 mapping 27: avar2 mapping from the default location is dropped: YTOS 11 -> 10
warn  4.2 /acute missing in 12 sources: anchor '_topacute' missing in 12 sources
3 problem(s), 2 structural

Exit code is 0 when clean, 1 when there are problems, 2 when the designspace cannot be read — so a build script can gate on it directly. A master that cannot be opened is reported and the others are still checked, and a check that fails partway is reported as 0.1 rather than dropped, so a clean result means every check ran.

Install

pip install designspace-lint            # the checks and the CLI
pip install "designspace-lint[features]"  # + the features.fea comparison (ufo2ft)
pip install "designspace-lint[interpolatable]"  # + a solver for --interpolatable

Point by point: --interpolatable

The glyph checks compare structure. Two masters can agree on every count and still interpolate badly: a contour that starts at another point or runs the other way twists between them, contours in another order swap shapes, a curve thins out or kinks halfway. varLib builds all of it without a word. --interpolatable (or lint(..., interpolatable=True)) runs fontTools' varLib.interpolatable over the masters already open and reports these as 4.14–4.17. It roughly doubles the run time, so it is off by default. Glyphs of more than six contours need an assignment solver; without the extra they are counted and reported once (4.18) instead of checked.

Use it from Python

from designspace_lint import lint_path

for problem in lint_path("Family.designspace"):
    print(problem.category, problem.code, problem.description)

When the fonts are already open — inside an editor, or a build script that has loaded them anyway — hand them over instead of paying to open them twice. Any object with path, doc and sources will do, where each source has font, path, name, location, layer_name and the layer accessors described by designspace_lint.protocols.SourceLike:

from designspace_lint import lint

problems = lint(my_designspace)     # nothing is re-opened

What it checks

Category What it looks at
0 File the document can be read at all, and every check could finish
1 Geometry axis minimum/default/maximum, mappings, duplicate names and tags, discrete axis values, avar2 mappings: from the default, duplicate inputs, unknown axes, clamped values, outputs past the masters, chains; the declared <variable-fonts>
2 Sources locations (as written in the file, before fontTools drops unknown axes), missing UFOs, duplicate locations as varLib compares them, the default, sources off a discrete axis' values
3 Instances instance locations and names, unknown location labels, one name at two locations
4 Glyphs master compatibility: contours, points, curve types, components, anchors, unicodes, contour direction, empty glyphs, how far along each axis a glyph actually reaches, and with --interpolatable start points, contour order and shapes that thin out or kink
5 Kerning kerning groups, masters with no kerning, a glyph in two groups of one side
6 Font Info units per em, required fields, values that differ
7 Rules rule conditions, rules that never apply, rule glyphs missing from the default master, overlapping rules
8 Features whether the masters' features.fea keep the build on the variable path
9 Glyph Order extra glyphs, and orders that break the per-master merge
10 Labels DS5 axis and location labels: the instance names the build derives from them, values no label covers, labels STAT drops, ranges and style links

The (category, code) pairs are a public contract. A retired check keeps its number, and new checks only ever get new ones.

The rules come from what the tools actually do

Every rule here was checked against fontTools and ufo2ft rather than assumed, and several of them contradict what seems reasonable:

  • A glyph may skip a master in the middle of an axis — varLib builds a per-glyph model from the masters that have it. Skipping the master at the end of an axis is the dangerous one: measured on a 0 / 0.5 / 1.0 axis carrying 100 / 150 / 300 with the last master missing, 0.75 gives 125 and 1.0 gives 100 instead of 225 and 300. The font builds without a word.
  • Differing kerning pair sets are normal and are not reported: a pair a master does not list resolves to its group value or to 0, which is exactly what its absence means in a UFO. A master with no kerning is reported, and what it does depends on ufo2ft: up to 3.8 every pair resolves to 0 there and the kerning sags toward it; since 3.9 the master is skipped and the kerning interpolates across it.
  • Groups without pairs do not help — verified by building a variable font and reading the values back.
  • ufo2ft compiles one variable feature file from the default master only when every other master's features.fea matches it or all of them are empty. A mix of the two is not compatible either, and falling off that path is silent.
  • Overlapping rules are not settled by declaration order. varLib makes one lookup per rule and numbers the lookups in sorted order of their substitutions; in the overlap, the first to rewrite a glyph wins. Declared a → a.zzz then a → a.aaa, the variable font shows a.aaa, while static instances, which apply rules in declaration order, show a.zzz.
  • A rule with no <conditionset> never applies, and fontTools drops it. An empty <conditionset/> is the spec's always-on rule.
  • An avar2 mapping from the default location is discarded. varLib keeps it only as the base of the variation store it then throws away, and measures every other mapping from it: with Weight 100/400/900, adding 400 → 650 to 900 → 700 moves normalized 1.0 from 0.6 to 0.1 and leaves the default where it was.
  • An avar2 mapping value past the axis end is clamped, and the mappings are evaluated at the coordinates the user set, before any of them applies: one mapping's output never feeds another's input. Two mappings whose inputs normalize to the same point stop the build, two default-input mappings on different axes included.
  • Instance names come from the labels. When the document has labels, the build names each instance from the labels at its location, and a stylename without xml:lang="en" is replaced by that in the variable font. Where every label is elidable and there is no elidedfallbackname, the name is empty.
  • Duplicate masters are duplicates after the omitted axes are filled in: {Weight: 900} and {Weight: 900, Width: 100} (the default) are one location to varLib, which refuses them.
  • An empty glyph is a missing glyph to the outline, not to the spacing. When the default draws a glyph, varLib reads an empty copy in another master as absent and interpolates the shape across it, while the advance width of that empty copy still interpolates. So it is not an incompatibility, and at the end of an axis it reverts like a missing glyph (4.11).
  • Sparse masters need no naming convention. Which masters a glyph may skip follows from the axes; which sources carry kerning, features and a glyph order of their own follows from whether they are layers.
  • A gap at the end of an axis only counts when the glyph varies on that axis. One that sits at a single coordinate carries no delta along it, so nothing fades out — that is a parametric family's normal arrangement, and a glyph drawn only in the default master is reported once as static (4.13) rather than once per axis end.
  • A designspace 5 source may name only the axes it is not default on; that is not a missing value.

Each of those last two was learned the hard way, by running this over Amstelvar — 91–93 axes, up to 150 masters — where the earlier readings produced 1518 and 18609 findings that all meant "this is how a parametric family is built". See the changelog for 0.1.1.

What it does not see

It never compiles anything, so whatever exists only in the built font is out of reach. The September 2026 coverage audit (docs/audit/) sorted these out; the main ones:

  • Features ufo2ft writes for you. kern, mark and mkmk are generated from kerning and anchors at build time. Category 8 compares the masters' features.fea text and cannot see what the writers would add or where they would disagree across masters.
  • What only the compiled tables show: lookup overflows and table sizes, TrueType hinting, name-table ID reuse, and the order the shaper applies feature variations in beyond what 7.7 predicts.
  • Renderers. avar2 is new; an engine that only reads avar1 ignores the whole table. Whether a given app or instancer handles a font correctly is not a property of the designspace.
  • Muting. mutedGlyphNames, muteKerning and muteInfo on instances are not taken into account.
  • Point correspondence only with --interpolatable. Even then, component transforms are compared by structure, not by what they do between masters.
  • Masters that could not be opened. They are reported (2.1) and left out; checks that measure extents, such as 4.11, then measure without them.

For those, build the font (fontmake, or fonttools varLib) and run varLib.interpolatable and your shaping tests on the result.

Relation to designspaceProblems

This started as a reimplementation of designspaceProblems by Erik van Blokland (LettError), MIT-licensed, and it still owes that project its shape: the idea of numbering problems by category and code, and most of the category-4 glyph compatibility checks, come from there. The numbering is kept deliberately compatible — codes 4.0 to 4.10 mean what they mean in designspaceProblems — so anyone moving across reads the same numbers.

It was rewritten rather than wrapped because of what using it in a font editor asked for, and those needs turned out to be structural:

  • Results as data, not as a report. Every problem is a dataclass carrying its location, the masters involved and the diagnosis, so a UI can group, filter and navigate them. The original returns a mapping of problem to glyph names, which is the right shape for a report and the wrong one for a list you click through.
  • No re-opening fonts. An editor already holds them; the checks take whatever satisfies the protocol.
  • Layer-based designspaces. Several masters in one UFO, told apart by layer, have to be compared on the layer they actually draw.
  • Progress and cancellation, because a family with 84 masters takes long enough that a person will want to stop it.
  • Consequences in the message. The checks grew to say what the build will do, which meant verifying each rule against varLib and ufo2ft — and finding a few where the reasonable-sounding rule is not the real one.

It is not a drop-in replacement: the entry points and the result type differ. For the glyph checks the vocabulary is the same.

License

Apache-2.0. See LICENSE, and NOTICE for the designspaceProblems attribution.

Metadata

Release files for designspace-lint 0.5.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 designspace-lint 0.5.0
File Size Uploaded
designspace_lint-0.5.0.tar.gz 107.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for designspace-lint 0.5.0
File Interpreter ABI Platform
designspace_lint-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 204.2 kB

Release files / designspace_lint-0.5.0.tar.gz

Download URL designspace_lint-0.5.0.tar.gz
Size 107.9 kB
Tags Source
SHA-256 checksum
How to use checksums
4469d484396238f0d477c77a652f70ef95bb2c96640096ec6d1340a98ce5ecc3
BLAKE2b-256 checksum
How to use checksums
3d312361d670a213821f263cfd2a2a516afc293e2d1fae7411c806dbe5f55bb2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.

Transparency log

Release files / designspace_lint-0.5.0-py3-none-any.whl

Download URL designspace_lint-0.5.0-py3-none-any.whl
Size 96.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
89d5c9e0f395531e5623592873ab47161e1b062bcedff3ccd60f6c3b3eeda62b
BLAKE2b-256 checksum
How to use checksums
70be63568b38e56f7f8e4579753177c1dde156080097d4a41d1084aeaab7533c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.

Transparency log

Release history Release notifications | RSS feed

0.7.0

2 release files

0.6.0

2 release files

This release

0.5.0 This release

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

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