Skip to content
exorientePublic

About

Immutable, typed intervals that behave like Python sets

Topics

Resources

Stars

3 stars

Watchers

1 watching

Forks

Repository files navigation

⟨ beset ⟩

typed intervals with the interface of Python sets

PyPI Python versions License Build Documentation

The beset Python library provides generic interval classes for use in typed Python. It is tested to work well with mypy, ty, pyright and pyrefly.

The interface of beset intervals mirrors that of Python set. If you know Python set operations, you know how to use this library.

Full documentation is available on Read the Docs.

Contents

Installation

beset is available on PyPI and can be installed using pip.

$ pip install beset

Popular package managers can install it out of the box. For instance, you can run poetry add beset or uv add beset if you use these tools.

Development status

The beset library is currently still in an early stage of development. Its design and API are likely to change in upcoming versions until version 1.0 is reached.

If maturity and stability are requirements, check out some other libraries that might meet your needs. Conversely, if you have requirements or needs you'd like to see the beset library satisfy or have feedback on the current design or API, any input or contribution is highly appreciated.

Quick demo

Let's assume beset is imported as follows:

>>> import beset as b

We can use one of the interval classes, such as ClosedOpen, to create intervals and perform standard set operations on them:

>>> working_hours = b.ClosedOpen(9, 17)
>>> lunch_break = b.ClosedOpen(12, 13)

>>> available = working_hours - lunch_break  # set subtraction operator

>>> print(available)
[9 ; 12) | [13 ; 17)

>>> meeting = b.ClosedOpen(11, 12)
>>> meeting <= available  # is a subset operator
True

Why use beset?

When writing typed Python code

Use beset when you need statically typed intervals. Its generic classes preserve bound types through interval and set operations and are understood by type checkers.

Adherence to the Python set interface

Adherence to the existing interface of Python set means you can jump in and start using this library with little prior knowledge.

Note: Since beset intervals are immutable it would be more accurate to say they follow the interface of frozenset, which itself matches most of the set interface.

Strict function signatures and variable definitions

beset classes allow you to strictly specify in function signatures and variable type hints what kinds of intervals your code expects and to use type checkers to enforce those expectations. For example, the Open[int] type hint limits objects to open continuous non-empty intervals defined on integer bounds, while ClosedOpenSet[datetime | None] allows bounded and unbounded multiintervals defined on date-times.

That means your code needs fewer run-time checks and error handling to deal with intervals of the wrong type or containing the wrong type of data.

Intervals work on many data types

beset intervals can be defined on any type of data that can be ordered using its less-than (<) operator. This not only includes int, float, datetime and similar types, but also types like str that have an ordering without a concept of distance between values. Intervals can be defined on your own classes as well, as long as you provide an implementation for the less-than operator.

Note: beset is not the only interval library providing this. See below for some others.

Intervals are immutable and hashable

beset interval objects are immutable and hashable, as long as the objects used as bounds can be hashed.

Some additional examples

Intervals can have open or closed bounds:

>>> print(b.Open(6, 7))
(6 ; 7)

>>> print(b.OpenClosed(6, 7))
(6 ; 7]

>>> print(b.ClosedOpen(6, 7))
[6 ; 7)

>>> print(b.Closed(6, 7))
[6 ; 7]

Intervals contain all possible values between their lower and upper bounds:

>>> 6 in b.ClosedOpen(7, 9)
False

>>> 7 in b.ClosedOpen(7, 9)
True

>>> 8 in b.ClosedOpen(7, 9)
True

>>> 9 in b.ClosedOpen(7, 9)
False

>>> 10 in b.ClosedOpen(7, 9)
False

beset intervals have methods and operators mirroring those of Python set (or frozenset more specifically, since beset intervals are immutable). Some examples:

>>> b.ClosedOpen(10, 20) & b.ClosedOpen(15, 25)  # intersection
ClosedOpen(15, 20)

>>> b.ClosedOpen(3, 9) < b.Open(0, 10)  # is proper subset
True

Set subtraction can lead to disjoint sets. The beset library represents these using the class IntervalSet.

>>> s = b.Open(0, 10) - b.Open(3, 5)

>>> s
IntervalSet([OpenClosed(0, 3), ClosedOpen(5, 10)])

>>> print(s)
(0 ; 3] | [5 ; 10)

You can also create an IntervalSet explicitly, but it's often easier to use the union operator on simple intervals. The results are the same.

>>> b.IntervalSet([b.Open(10, 20), b.Open(30, 40)]) == b.Open(10, 20) | b.Open(30, 40)
True

The beset library supports unbounded intervals without upper or lower bound. Create such intervals by using None as a bound.

>>> x = b.Closed(10, None)
>>> print(x)
[10 ; +inf⟩

>>> 100 in x
True

Unbounded intervals allow for the introduction of the complement operation that returns the complementary interval, containing everything not in the original interval.

>>> b.Closed(-3, 7).complement()
OpenSet([RightOpen(-3), LeftOpen(7)])

>>> print(~b.ClosedOpen(0, 100))  # the ~-operator returns the complement
⟨-inf ; 0) | [100 ; +inf⟩

Typing

The beset classes are generic. Type checkers automatically infer the correct type.

>>> reveal_type(b.ClosedOpen(2.718, 6.283))  # Revealed type is beset.ClosedOpen[float]

Featured types

You can use the beset classes to specify precisely what kind of interval your code expects. In the following inventory of classes we assume intervals with int bounds, but any suitable type will work.

a: b.IntervalSet[int | None]

IntervalSet matches any type of interval; atomic (single) intervals as well as unions of intervals or the empty interval. Including None will mean unbounded intervals, with plus or minus infinity as bounds, are also permitted.

b: b.IntervalSet[int]

Using a stricter IntervalSet, without None, will allow only bounded intervals, multiintervals and the empty interval, but no unbounded ones.

c: b.Interval[int | None]

The Interval class matches any type of atomic interval (Closed, Open, ClosedOpen, OpenClosed) and will allow unbounded ones as well, but not empty ones.

d: b.Interval[int]

The Interval class matches any type of interval (Closed, Open, ClosedOpen, OpenClosed) but will not allow unbounded intervals or empty ones.

e: b.Interval[int] | b.Empty

This matches all atomic intervals as well as the empty one. Note that the empty interval class takes no type argument because it has no bounds.

f: b.ClosedOpen[int]

Some applications strictly use ClosedOpen intervals.

g: b.ClosedOpenSet[int]

Union sets of closed-open intervals can also be specified. Such classes exist also for closed, open and open-closed intervals.

Intersections to get rid of None

Operations on intervals may return different types of intervals or intervals with different type arguments. For example, taking the complement of an Interval[int] will give you an IntervalSet[int | None].

>>> x = ~b.Closed(0, 10)
>>> print(x)
⟨-inf ; 0) | (10 ; +inf⟩

>>> reveal_type(x)  # Revealed type is beset.IntervalSet[int | None]

Getting rid of the union with None can be accomplished using the intersection operation. Continuing the example:

>>> y = x & b.Closed(-100, 100)
>>> print(y)
[-100 ; 0) | (10 ; 100]

>>> reveal_type(y)  # Revealed type is beset.IntervalSet[int]

Other libraries

There are already excellent Python libraries available that provide interval data structures and operations. They may suit your use case better, depending on your needs.

  • portion
    • A very mature interval library (and one of the main inspirations for beset)
  • intervaltree
    • Focused on speed and performance for large interval sets

About

Immutable, typed intervals that behave like Python sets

Topics

Resources

Stars

3 stars

Watchers

1 watching

Forks

Used by

Contributors

Languages