Pure-Python Unicode-aware casing for Turkic Latin text
Project description
turkicase
turkicase is a small, dependency-free Python package for Unicode-aware casing of Turkic Latin text.
It implements the Turkish and Azerbaijani special-casing rules for the two distinct Latin I pairs:
| Uppercase | Lowercase |
|---|---|
I |
ı |
İ |
i |
Python's built-in casing methods are intentionally locale-independent:
"I".lower() # "i"
"i".upper() # "I"
For Turkish, Azerbaijani, and other text using Turkic Latin casing rules, the expected mappings are instead:
I ↔ ı
İ ↔ i
turkicase applies these Turkic-specific mappings while delegating all unrelated Unicode casing behavior to Python.
Installation
Install the latest release from PyPI:
python -m pip install turkicase
Quick start
from turkicase import lower, upper, casefold
lower("IĞDIR") # "ığdır"
lower("İSTANBUL") # "istanbul"
upper("izmir") # "İZMİR"
upper("ışık") # "IŞIK"
Public API
The package provides three primary functions:
lower(text, *, normalization="NFC")
upper(text, *, normalization="NFC")
casefold(text, *, normalization="NFC")
The normalization argument is keyword-only and accepts:
"NFC"
"NFD"
"NFKC"
"NFKD"
None
The default is "NFC".
lower()
lower(text, *, normalization="NFC")
Returns a Turkic-aware lowercase version of text.
The core mappings are:
I → ı
İ → i
Examples:
from turkicase import lower
lower("I") # "ı"
lower("İ") # "i"
lower("IĞDIR") # "ığdır"
lower("İSTANBUL") # "istanbul"
Decomposed dotted I
Unicode text does not always represent dotted capital I as the single code point İ (U+0130).
It may instead use a decomposed sequence:
I + ◌̇
U+0049 + U+0307
This sequence must lowercase to a single ordinary i:
lower("I\u0307") # "i"
It must not become:
ı + ◌̇
The implementation handles this using Unicode's contextual Before_Dot and After_I rules.
Before_Dot
When processing a capital I, the implementation scans forward in the original input for COMBINING DOT ABOVE (U+0307).
The dot may be reached through intervening combining characters whose canonical combining class is neither 0 nor 230.
A character with canonical combining class 0 or 230 blocks the context.
Conceptually:
I + ◌̇
→ i
But with a blocking character:
I + blocker + ◌̇
→ ı + blocker + ◌̇
The target U+0307 itself is recognized before its combining class is considered as a blocker.
After_I
When processing U+0307, the implementation scans backward through the original input.
If the dot belongs contextually to an earlier capital I, the dot is deleted during Turkic lowercasing:
I + ◌̇
↓
i
A character with canonical combining class 0 or 230 blocks this backward relationship.
Original input is not normalized first
Casing contexts are evaluated against the original sequence of input code points.
The implementation does not normalize the input before applying Before_Dot or After_I.
This is important because pre-normalization could compose or rearrange a sequence and hide the contextual relationship that the casing algorithm needs to inspect.
The processing order is therefore:
original input code points
→ evaluate Turkic casing contexts
→ apply casing
→ normalize the result, if requested
Other Unicode lowercase behavior
After the Turkic I rules have been applied, the transformed string is passed to Python's whole-string str.lower() implementation.
This preserves Python's normal Unicode behavior for unrelated scripts, including context-sensitive behavior such as Greek final sigma.
upper()
upper(text, *, normalization="NFC")
Returns a Turkic-aware uppercase version of text.
The core mappings are:
i → İ
ı → I
Examples:
from turkicase import upper
upper("i") # "İ"
upper("ı") # "I"
upper("izmir") # "İZMİR"
upper("ışık") # "IŞIK"
Internally, ordinary small i characters are first changed to capital dotted İ.
Python's whole-string str.upper() then handles dotless ı and all unrelated Unicode uppercase behavior.
casefold()
casefold(text, *, normalization="NFC")
Returns a Turkic-aware caseless-comparison key.
It first applies Turkic lowercase rules and then applies Python's full Unicode str.casefold() operation.
Use casefold() for comparison, searching, indexing, and deduplication—not for presentation text.
from turkicase import casefold
casefold("I") == casefold("ı") # True
casefold("İ") == casefold("i") # True
casefold("I") == casefold("i") # False
The two Turkic I pairs therefore remain distinct:
I ↔ ı
İ ↔ i
Python's full case folding also remains active for unrelated characters:
casefold("Straße") # "strasse"
Because case folding can expand or otherwise transform characters, its result should be treated as a comparison key rather than user-facing text.
Unicode normalization
All three public functions accept a normalization argument.
NFC
normalization="NFC"
Canonical decomposition followed by canonical composition.
This is the default and is generally appropriate for normal storage and display text.
upper("i", normalization="NFC") # "İ" — U+0130
NFD
normalization="NFD"
Canonical decomposition without recomposition.
upper("i", normalization="NFD")
# "I\u0307"
# U+0049 + U+0307
The NFC and NFD results are canonically equivalent but have different code-point sequences.
NFKC
normalization="NFKC"
Compatibility decomposition followed by canonical composition.
NFKC may change characters unrelated to Turkic casing, including fullwidth forms, ligatures, circled numbers, superscripts, and other compatibility characters.
lower("I①A", normalization="NFKC")
# "ı1a"
In this example:
I → ı
① → 1
A → A → a
Use NFKC when compatibility distinctions should be folded for matching or normalized input.
Do not use it when preserving the original typographic form is important.
NFKD
normalization="NFKD"
Compatibility decomposition without canonical recomposition.
Like NFKC, it may remove compatibility distinctions, but decomposable characters remain in decomposed form.
No normalization
normalization=None
Skips the final requested normalization step.
result = lower(text, normalization=None)
This does not mean the original code-point sequence is preserved unchanged: casing itself may replace, expand, or delete characters.
It only means that no additional unicodedata.normalize() operation is requested for the result.
Normalization summary
| Value | Compatibility folding | Final form |
|---|---|---|
"NFC" |
No | Canonically composed |
"NFD" |
No | Canonically decomposed |
"NFKC" |
Yes | Compatibility-folded and composed |
"NFKD" |
Yes | Compatibility-folded and decomposed |
None |
No requested normalization | Casing result as produced |
For most presentation text, use the default "NFC".
For comparison pipelines that deliberately fold compatibility characters, consider "NFKC".
Longer aliases
Longer descriptive aliases are also exported:
from turkicase import (
common_turkic_lower,
common_turkic_upper,
common_turkic_casefold,
)
They are aliases of the shorter functions:
common_turkic_lower is lower
common_turkic_upper is upper
common_turkic_casefold is casefold
Scope
turkicase:
- always applies Turkic casing semantics;
- does not detect the language or locale automatically;
- specializes the Latin
I,İ,i, andımappings; - implements contextual handling of decomposed
I + U+0307; - delegates unrelated Unicode casing behavior to Python;
- has no runtime dependencies;
- includes typing information through
py.typed.
Use it when the text is known to require Turkish, Azerbaijani, or compatible Common Turkic Latin casing behavior.
Do not apply Turkic casing indiscriminately to text whose language requires the default Unicode mapping I ↔ i.
Development
Install the project and its development dependencies:
python -m pip install -e ".[dev]"
Run the test suite:
python -m pytest
Remove previous build artifacts before producing a new release:
rm -rf -- dist
rm -rf -- build
rm -rf -- src/turkicase.egg-info
Build the source distribution and wheel:
python -m build
Check the generated distributions:
python -m twine check --strict dist/*
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file turkicase-0.1.1.tar.gz.
File metadata
- Download URL: turkicase-0.1.1.tar.gz
- Upload date:
- Size: 7.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
79eea6af30216dd9dc0a91f157fbf321a11cba71bec85f145a2670957d3d94ff
|
|
| MD5 |
e91f3c1556c647c2cc763ecc2eb23b1c
|
|
| BLAKE2b-256 |
1131b6123a66515031808ff020d86b4b458aedd8148fc7a190554dab6d924a49
|
File details
Details for the file turkicase-0.1.1-py3-none-any.whl.
File metadata
- Download URL: turkicase-0.1.1-py3-none-any.whl
- Upload date:
- Size: 7.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ee7603874592b18df7d1c208352496fa1f1dd0446a8ffb975e471774ac37b3d3
|
|
| MD5 |
1f1739ad185069d82090e935f05f3ba8
|
|
| BLAKE2b-256 |
1621ac4dd4e0f579fc52054079f766b857759d82219c3ed8e00e2852989819f6
|