Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ Changelog

Versions follow `Semantic Versioning <http://www.semver.org>`_

`Unreleased`_
-------------
Fixed
~~~~~
* :meth:`.ULID.from_str` accepts lower case strings again, as the ULID spec defines the string
representation as case insensitive. Lower case input has been rejected since 2.6.0.

`4.0.1`_ - 2026-07-20
---------------------
Changed
Expand Down Expand Up @@ -265,6 +272,7 @@ Changed
* The package now has no external dependencies.
* The test-coverage has been raised to 100%.

.. _Unreleased: https://github.com/mdomke/python-ulid/compare/4.0.1...HEAD
.. _4.0.1: https://github.com/mdomke/python-ulid/compare/4.0.0...4.0.1
.. _4.0.0: https://github.com/mdomke/python-ulid/compare/3.2.1...4.0.0
.. _3.2.1: https://github.com/mdomke/python-ulid/compare/3.2.0...3.2.1
Expand Down
7 changes: 7 additions & 0 deletions tests/test_ulid.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,12 @@ def test_idempotency() -> None:
assert ULID.parse(ulid.bytes) == ulid


def test_from_str_is_case_insensitive() -> None:
ulid = ULID()
assert ULID.from_str(str(ulid).lower()) == ulid
assert ULID.parse(str(ulid).lower()) == ulid


def test_to_uuid4() -> None:
ulid = ULID()
uuid = ulid.to_uuid4()
Expand Down Expand Up @@ -350,6 +356,7 @@ def test_ulid_from_timestamp_overflow() -> None:
(ULID.from_str, "not-enough"), # invalid length
(ULID.from_str, 123), # inavlid type
(ULID.from_str, "notavalidulidnotavalidulid"), # invalid alphabet
(ULID.from_str, "0" * 25 + "u"), # excluded letter in lower case
(ULID.from_str, "Z" * 26), # invalid timestamp
(ULID.from_int, "not-an-int"), # invalid type
(ULID.from_uuid, "not-a-uuid"), # invalid type
Expand Down
4 changes: 3 additions & 1 deletion ulid/base32.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
# https://github.com/RobThree/NUlid/blob/89f5a9fc827d191ae5adafe42547575ed3a47723/NUlid/Ulid.cs#L168

ENCODE: str = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
# The ULID spec defines the string representation as case insensitive.
DECODE_ALPHABET: str = ENCODE + ENCODE.lower()
DECODE: Sequence[int] = [
0xFF,
0xFF,
Expand Down Expand Up @@ -194,7 +196,7 @@ def encode_randomness(binary: bytes) -> str:
def decode(encoded: str) -> bytes:
if len(encoded) != constants.REPR_LEN:
raise ValueError("Encoded ULID has to be exactly 26 characters long.")
if any((c not in ENCODE) for c in encoded):
if any((c not in DECODE_ALPHABET) for c in encoded):
raise ValueError(f"Encoded ULID can only consist of letters in {ENCODE}.")
return decode_timestamp(encoded[: constants.TIMESTAMP_REPR_LEN]) + decode_randomness(
encoded[constants.TIMESTAMP_REPR_LEN :]
Expand Down