Issue Nº 01 — October 15, 2026

@readmodelmag
MODEL

Follow MODEL on X @readmodelmag

Tools

admins and аdmins are two different variables. Here's how to catch lookalike names before they ship.

SummaryOne Cyrillic letter can give a Python variable a twin that looks the same in a diff. Python runs it without a word, and Ruff's default rules pass it. We tested which checks catch it and which don't.

· MODEL Editors · 3 min read

Some links may be affiliate links. Disclosure

MODEL builder tip card titled admins and аdmins are two different variables. Python runs it, Ruff's default rules pass it, two opt-in rules catch it. Left panel, bypass.py on Python 3.13.5: admins is set to alice, then аdmins, spelled with a Cyrillic small letter a (U+0430), is set to admins plus mallory, and the function returns user in аdmins. Running it prints mallory can_delete: True, and ruff check with default rules prints All checks passed. Verdict: no warning anywhere. Right panel, ruff 0.17.2 and rustc 1.85.1: ruff check with PLC2401 and RUF001 selected reports PLC2401, variable name аdmins contains a non-ASCII character, 2 errors in 2 files, 0.02 seconds, exit 1. rustc with no flags warns that both admins and аdmins exist as identifiers which look alike, but still compiles and runs. Verdict: caught. Below: Python folds names to NFKC, so a fullwidth i in is_admin = True silently overwrote is_admin. Fix: add PLC2401, PLC2403, RUF001, RUF002 and RUF003 to extend-select in pyproject.toml and run it in CI. Catch: PLC2401 flags every non-ASCII name, including café, and Rust only warns. Fallback: a git diff grep for non-ASCII added lines. Footer: tested by MODEL Editors, October 25, 2026.
Our own terminal output from October 25, 2026, trimmed. Illustration by MODEL. Original. Image source.

The Unicode Consortium's security report, UTS #39, was on the Hacker News front page today. It's the document behind "confusables": characters from different scripts that look the same, like the Latin a and the Cyrillic а (U+0430). The report covers how to detect them. Most of the attention goes to lookalike domain names, but the same trick works in source code. More code now arrives from agents and drive-by pull requests, and nobody reads it letter by letter. So we checked which tools notice a lookalike name.

The test. We wrote a five-line Python function:

def can_delete(user):
    admins = {"alice"}
    аdmins = admins | {"mallory"}
    return user in аdmins

The third and fourth lines use a Cyrillic а. In a terminal, an editor or a diff, it reads like a harmless line that the return statement ignores. Python treats аdmins as a separate variable, so the function checks the list with mallory in it. python3 bypass.py printed mallory can_delete: True. Python 3.13.5 gave no warning.

Python also merges some names you'd think were different. The Python docs say all names are converted to the normal form NFKC while parsing. So a fullwidth i (U+FF49) folds to a plain i. In our second file, is_admin = True silently overwrote is_admin = False, and the script printed True. Cyrillic letters don't fold, so аdmins stays separate. Either way, the code doesn't do what it looks like it does.

What caught it:

  • Ruff 0.17.2 with its default rules: nothing. ruff check printed "All checks passed!" on both files.
  • Ruff with two opt-in rules: caught. ruff check --select PLC2401,PLC2403,RUF001,RUF002,RUF003 flagged аdmins and is_admin as PLC2401 ("Variable name contains a non-ASCII character") and exited 1. It took 0.02 seconds on our two files. RUF001 caught a Cyrillic а inside a string and named it: "Did you mean a (LATIN SMALL LETTER A)?" PLC2403 covers import names, and RUF002 and RUF003 cover docstrings and comments.
  • Rust: caught by default. We ported the same trick to Rust. rustc 1.85.1 with no flags warned "found both admins and аdmins as identifiers, which look alike" (lint confusable_idents). It added a mixed_script_confusables warning naming U+0430. Both lints are on by default.
  • A plain grep works for any language. grep -nP '[^\x00-\x7F]' printed both lines. To check only what you're about to commit, run git diff --cached -U0 | grep -P '^\+(?!\+\+).*[^\x00-\x7F]'. It flagged the two added lines in our test repo.

The tip. For Python, put the rules in your config so they run everywhere, including CI and your agent's own lint step:

[tool.ruff.lint]
extend-select = ["PLC2401", "PLC2403", "RUF001", "RUF002", "RUF003"]

With that in pyproject.toml, a plain ruff check flagged the committed file and exited 1.

The catches.

  • PLC2401 is a blunt rule. It flags every non-ASCII name, not just lookalikes. It flagged café = 1 too. If your team uses non-English names on purpose, expect noise or add per-file ignores.
  • Rust only warns. The program still compiled, ran and printed true. Add #![deny(confusable_idents, mixed_script_confusables)], or fail CI on warnings, if you want it to block a merge.
  • The grep catches every non-ASCII character, including emoji and accented text in strings. It's a review prompt, not a verdict.
  • We didn't test JavaScript, TypeScript or Go tooling, editors' own Unicode highlighting, or GitHub's diff view.

Our take

Two minutes of config. If agents or outside contributors write Python for you, turn on those five Ruff rules today. In Rust, make the two lints fail the build. Then a reviewer can trust that a name that looks the same is the same.

What we did: we ran everything above on a Linux box on October 25, 2026, with Python 3.13.5, Ruff 0.17.2 (via uvx), rustc 1.85.1 and git 2.47.3, on small test files we wrote. Facts come from UTS #39, the Python language reference, PEP 672, Ruff's rule docs and the rustc lint listing, all read today. Not sponsored; no affiliate links.

MODEL Editors

Sources

  1. UTS #39: Unicode Security Mechanisms (version 18.0.0)

    Unicode Consortium

  2. UTS #39: Unicode Security Mechanisms (Hacker News discussion)

    Hacker News

  3. Lexical analysis: Names (identifiers and keywords)

    Python documentation

  4. PEP 672: Unicode-related Security Considerations for Python

    Python Software Foundation

  5. non-ascii-name (PLC2401)

    Ruff documentation

  6. ambiguous-unicode-character-string (RUF001)

    Ruff documentation

  7. Ruff 0.17.2 release

    astral-sh/ruff on GitHub

  8. Warn-by-default lints: confusable_idents, mixed_script_confusables

    The rustc book

FREE · EVERY WEEK

The top AI tools this week, tested.

Every week MODEL's editors pick the AI tools worth your time, try them hands-on, and tell you what's good, what's not, and who it's for. Free, in your inbox.

Free. Unsubscribe anytime. Our picks are editorial and can't be bought.

The weekly three is editorial. A subscription does not buy a place in it.