Awesome implementation on Requests:

https://github.com/kennethreitz/requests/blob/v1.2.3/requests/structures.py#L37

Answer from santiagobasulto on Stack Overflow
🌐
GitHub
github.com › tivvit › python-case-insensitive-dict
GitHub - tivvit/python-case-insensitive-dict: Python case insensitive dictionary · GitHub
from CaseInsensitiveDict import CaseInsensitiveDict cid = CaseInsensitiveDict({"A": {"A": 1}, "B": 2, "c": 3}) print cid["A"] # >>> {'a': 1} print cid["a"] # >>> {'a': 1} print cid["A"]["a"] # >>> 1 print cid["b"] # >>> 2 print cid["C"] # >>> 3
Starred by 8 users
Forked by 2 users
Languages: Python
Top answer
1 of 13
100

Awesome implementation on Requests:

https://github.com/kennethreitz/requests/blob/v1.2.3/requests/structures.py#L37

2 of 13
94

The answer by jkp wouldn't work for lots of cases, so it cannot be used as a drop-in dict replacement. Some tricky points in getting a proper dict replacement:

  • overloading all of the methods that involve keys
  • properly handling non-string keys
  • properly handling the constructor of the class

The following should work much better:

class CaseInsensitiveDict(dict):
    @classmethod
    def _k(cls, key):
        return key.lower() if isinstance(key, basestring) else key

    def __init__(self, *args, **kwargs):
        super(CaseInsensitiveDict, self).__init__(*args, **kwargs)
        self._convert_keys()
    def __getitem__(self, key):
        return super(CaseInsensitiveDict, self).__getitem__(self.__class__._k(key))
    def __setitem__(self, key, value):
        super(CaseInsensitiveDict, self).__setitem__(self.__class__._k(key), value)
    def __delitem__(self, key):
        return super(CaseInsensitiveDict, self).__delitem__(self.__class__._k(key))
    def __contains__(self, key):
        return super(CaseInsensitiveDict, self).__contains__(self.__class__._k(key))
    def has_key(self, key):
        return super(CaseInsensitiveDict, self).has_key(self.__class__._k(key))
    def pop(self, key, *args, **kwargs):
        return super(CaseInsensitiveDict, self).pop(self.__class__._k(key), *args, **kwargs)
    def get(self, key, *args, **kwargs):
        return super(CaseInsensitiveDict, self).get(self.__class__._k(key), *args, **kwargs)
    def setdefault(self, key, *args, **kwargs):
        return super(CaseInsensitiveDict, self).setdefault(self.__class__._k(key), *args, **kwargs)
    def update(self, E={}, **F):
        super(CaseInsensitiveDict, self).update(self.__class__(E))
        super(CaseInsensitiveDict, self).update(self.__class__(**F))
    def _convert_keys(self):
        for k in list(self.keys()):
            v = super(CaseInsensitiveDict, self).pop(k)
            self.__setitem__(k, v)

Note: If you're using Python 3, basestring has been removed, but str can be used as a replacement.

Discussions

performance - Python case insensitive dictionary - Code Review Stack Exchange
This is a Python case insensitive dictionary that is ordered and has integer indexes for the keys and values. I just wrote it today. It is ordered because I am using Python 3.9.6 and plain dict is More on codereview.stackexchange.com
🌐 codereview.stackexchange.com
Dictionary, Keys, casefold()
How about manipulating the input to be comparable with the dictionary key instead of manipulating the key? There are several handy string methods you can use. Lookup for example the method '.title' More on reddit.com
🌐 r/cs50
4
1
December 29, 2023
python - Case-insensitive dictionary check with lower() - Stack Overflow
I'm trying to use lower() so the role names are not case sensitive. So if a user types lol instead of LoL it won't go through the if statement if not role_id: This is how I'm doing it: @commands. More on stackoverflow.com
🌐 stackoverflow.com
Case insensitive dictionary? - Post.Byes - Bytes
Re: Case insensitive dictionary? Thanks! In my case I know for sure, that keys are strings. So I fill the dictionary with keys as they come to me from the source (preserve the case). Once the dictionary is filled, it is a "READ ONLY" object. In other words: there is no group operation writing ... More on post.bytes.com
🌐 post.bytes.com
Top answer
1 of 6
58

Note that making a dictionary case-insensitive, by whatever mean, may well lose information: for example, how would you "case-insensitivize" {'a': 23, 'A': 45}?! If all you care is where a key is in the dict or not (i.e., don't care about what value corresponds to it), then make a set instead -- i.e.

theset = set(k.lower() for k in thedict)

(in every version of Python, or {k.lower() for k in thedict} if you're happy with your code working only in Python 2.7 or later for the sake of some purely decorative syntax sugar;-), and check with if k.lower() in theset: ....

Or, you could make a wrapper class, e.g., maybe a read-only one...:

import collections

class CaseInsensitiveDict(collections.Mapping):
    def __init__(self, d):
        self._d = d
        self._s = dict((k.lower(), k) for k in d)
    def __contains__(self, k):
        return k.lower() in self._s
    def __len__(self):
        return len(self._s)
    def __iter__(self):
        return iter(self._s)
    def __getitem__(self, k):
        return self._d[self._s[k.lower()]]
    def actual_key_case(self, k):
        return self._s.get(k.lower())

This will keep (without actually altering the original dictionary, so all precise information can still be retrieve for it, if and when needed) an arbitrary one of possibly-multiple values for keys that "collapse" into a single key due to the case-insensitiveness, and offer all read-only methods of dictionaries (with string keys, only) plus an actual_key_case method returning the actual case mix used for any given string key (or None if no case-alteration of that given string key matches any key in the dictionary).

2 of 6
31

Start using a real case insensitive dictionary via:

from requests.structures import CaseInsensitiveDict

Or if you want to see the code:

class CaseInsensitiveDict(dict):

    """Basic case insensitive dict with strings only keys."""

    proxy = {}

    def __init__(self, data):
        self.proxy = dict((k.lower(), k) for k in data)
        for k in data:
            self[k] = data[k]

    def __contains__(self, k):
        return k.lower() in self.proxy

    def __delitem__(self, k):
        key = self.proxy[k.lower()]
        super(CaseInsensitiveDict, self).__delitem__(key)
        del self.proxy[k.lower()]

    def __getitem__(self, k):
        key = self.proxy[k.lower()]
        return super(CaseInsensitiveDict, self).__getitem__(key)

    def get(self, k, default=None):
        return self[k] if k in self else default

    def __setitem__(self, k, v):
        super(CaseInsensitiveDict, self).__setitem__(k, v)
        self.proxy[k.lower()] = k
🌐
Mathspp
mathspp.com › blog › case-insensitive-dictionary
Implementing a case-insensitive dictionary | mathspp
January 29, 2023 - How do you implement a case-insensitive (or caseless) dictionary? In this article we explore solutions where we inherit from the built-in dict, the abstract base class MutableMapping from the standard module collections.abc, and the UserDict in the standard module collections.
🌐
GitHub
github.com › pywbem › nocasedict
GitHub - pywbem/nocasedict: A case-insensitive ordered dictionary for Python · GitHub
Class NocaseDict is a case-insensitive ordered dictionary that preserves the original lexical case of its keys. ... $ python >>> from nocasedict import NocaseDict >>> dict1 = NocaseDict({'Alpha': 1, 'Beta': 2}) >>> dict1['ALPHA'] # Lookup by ...
Author: pywbem
🌐
PyPI
pypi.org › project › case-insensitive-dictionary
case-insensitive-dictionary
February 2, 2022 - JavaScript is disabled in your browser. Please enable JavaScript to proceed · A required part of this site couldn’t load. This may be due to a browser extension, network issues, or browser settings. Please check your connection, disable any ad blockers, or try using a different browser
Top answer
1 of 1
3

How can I make a custom data structure as fast as dict?

You can't (at least, not in CPython). CPython's dict is written in C, as are other dict variants in the standard library such as collections.defaultdict. If you want to write custom data structures approaching the speed of a dict, go write it in C, Rust, or run your Python script using PyPy. Anything subclassing a dict becomes an object implemented in Python rather than C, so will not run nearly as fast as a dict.

How can I make my code better?

Your code at the moment is extremely confusing. Here are some of the reasons why:

  1. You don't have any docstrings. It's really hard, as an outside reader, to understand what's going on in your code if you don't say what a specific function is for, and what its expected behaviour is. Type hints are also very helpful in this regard.
  2. You don't have any inline comments. Similar to point (1). There's all sorts of things going on in your code inside functions that are frankly a little head-scratching. There's probably a really good reason for a lot of them -- so if there's something that isn't self-explanatory happening inside a function, tell us why it's happening!
  3. It's really unclear why so many of your methods are private. What's the specific reason that the _key method is a private method? What would be so terrible about exposing that detail to other users? You have a lot of private methods, and the general expectation in Python is that a method should be public unless there's a good reason why.
  4. Your attributes are very confusingly named. Your UDict class has one instance attribute self.Keys, one instance attribute self.KEYS, one instance method self.keys(), and one instance method self._key(). This would be bad even if they recorded similar kinds of information, but they don't! One is a list, one is a dict, one is a method that returns a KeysView, and one is a method that transforms an "unprocessed" key into a "processed" one. How can anybody be expected to remember the difference between all of these?! There's really nothing wrong with being a bit more verbose and giving your attributes and methods more helpful, descriptive names. Your second attempt in your question improves on this a little, but not much. Why are your classes called IString and IDict? How can anybody be expected to know what they do from their names?
  5. The motivation for your class still seems a little unclear. You say it's a case-insensitive dict. Fine. But you also have added methods that will return the "integer index" of a key or value. It's unclear why these are necessary for this use case. Ditto for your multiget and multipop methods -- what are these for? And it's unclear why you would implement multiget and multipop, but not multisetdefault and multipopitem. What's the rationale here?

In addition to the above critiques, I would have started in a different place when it comes to solving this problem. My general attitude is that subclassing dict directly works great for when you're extending a dict -- just adding new features onto it, for a specialised purpose. But if you start overriding all sorts of dict dunder methods, I would generally look at inheriting from collections.abc.MutableMapping instead. The great thing about inheriting from MutableMapping is that by implementing just a few abstractmethods that MutableMapping has, you get a whole host of normal dict methods for free. In my example code below, I haven't had to override .setdefault(), .pop(), .popitem(), .get(), .keys(), .values(), .items(), .clear(), .update(), __contains__() or .__eq__(). They all come "for free" because I'm inheriting from MutableMapping. It just ends up being a lot cleaner, in my opinion.

I've also implemented a helper-class, to deal with some of the implementation-logic regarding case-insensitivity. There was just too much going on in your class, in my opinion; it made sense to shunt some of that code elsewhere.

I don't think there's a great deal of speed improvement in my refactoring below. I think in a few situations, my refactoring is a little faster than yours, but I haven't rigorously tested performance.

My code comes out as being a fair bit longer than yours, but I think the main reason for that is simply because I have a lot more comments and docstrings than you did...

from __future__ import annotations
from typing import Any, TypeVar, Optional, Union, NoReturn
from collections.abc import Mapping, MutableMapping, Iterator, Sequence
from abc import abstractmethod
from functools import cache


# For mapping keys and values
_K = TypeVar('_K')
_V = TypeVar('_V')

# Any subclass of _DictBoilerplateBase
_T = TypeVar('_T', bound='_DictBoilerplateBase[Any, Any]')

# For .get() defaults
_D = TypeVar('_D')


class _DictBoilerplateBase(MutableMapping[_K, _V]):
    """Base class for the mapping objects _KeyMapperDict and CaseInsensitiveDict.

    Inheriting from MutableMapping
    means the following methods are auto-implemented,
    both for this base class
    and for all classes inheriting from it:
        - .setdefault()
        - .pop()
        - .popitem()
        - .get()
        - .keys()
        - .values()
        - .items()
        - .clear()
        - .update()
        - .__contains__()
        - .__eq__()

    The following abstractmethods need to be implemented by all subclasses of this class:
        - .__init__()
        - .__getitem__()
        - .__setitem__()
        - .__delitem__()
    """

    __slots__ = 'data'
    
    data: dict[_K, _V] # For type-checkers

    @abstractmethod
    def __init__(
        self,
        data: Optional[Mapping[_K, _V]] = None,
        /,
        **kwargs
    ) -> None:
        
        if data is not None:
            self.update(data)
        if kwargs:
            self.update(kwargs)

    @classmethod
    def fromkeys(
        cls: type[_T],
        keys: Sequence[_K],
        default: Optional[_V] = None,
        /
    ) -> _T:
        """Standard alternative constructor for python mapping objects"""
        return cls({key: default for key in keys})
    
    def __iter__(self, /) -> Iterator[_K]:
        return iter(self.data.keys())
    
    def __len__(self, /) -> int:
        return len(self.data)

    def __repr__(self, /) -> str:
        return f'{type(self).__qualname__}({self.data!r})'

    def __str__(self, /) -> str:
        return f'{type(self).__name__}({self.data})'

    def __or__(self: _T, other: Mapping[_K, _V], /) -> _T:
        # (Dicts from python 3.9+ support the union operator)
        return type(self)({**self, **other})

    def __ror__(self: _T, other: Mapping[_K, _V], /) -> _T:
        # (Dicts from python 3.9+ support the union operator)
        return type(other)({**other, **self})

    def __ior__(self: _T, other: Mapping[_K, _V], /) -> _T:
        # (Dicts from python 3.9+ support the union operator)
        self.update(other)
        return self

    def copy(self: _T, /) -> _T:
        """Return a shallow copy of the mapping"""
        return type(self)(self)


class _KeyMapperDict(_DictBoilerplateBase[_K, _K]):
    """Helper class for the CaseInsensitiveDict class.

    Maps lowercase keys to the original case
    in which they were entered in the CaseInsensitiveDict.
    
    Also keeps the processed keys in a separate list,
    so that the integer index can be retrieved.

    This class knows NOTHING about the values
    in the CaseInsensitiveDict.

    All dunder methods expect to receive preprocessed keys.
    """

    __slots__ = 'keys_list'

    def __init__(
        self,
        data: Optional[Mapping[_K, _K]] = None,
        /,
        **kwargs
    ) -> None:
        # A map of lowercase-to-originalcase-keys
        self.data: dict[_K, _K] = {}
        
        # A list of the processed keys, for integer indexing
        self.keys_list: list[_K] = []

        super().__init__(data, **kwargs)

    @staticmethod
    @cache
    def process_key(key: _K, /) -> _K:
        """Normalise a key to allow for case-insensitivity when dealing with strings.
        Keys that are not strings are returned unchanged.
        """
        return key.lower() if isinstance(key, str) else key  # type: ignore[return-value]

    def register_key(
        self,
        unprocessed_key: _K,
        /
    ) -> Union[tuple[_K, int], tuple[None, None]]:
        """Determine whether a key is in the mapping, return its integer index if so.

        Parameters
        ----------
        unprocessed_key, _K:
            A key that has not yet been normalised
            according to this class's process_key method.

        Return
        ------
        If some version of the key is already in the mapping, returns (_K, int),
        a tuple consisting of:
            - The key in the form it was in
              when it was first entered in the CaseInsensitiveDict.
            - The integer index of the key in the mapping.

        If the key was not already in the mapping,
        returns:
            - (None, None)

        Examples
        --------
        >>> d = _KeyMapperDict({'spam': 'SPAM', 'eggs': 'eGgS'})
        >>> d.register_key('SPAM')
        ('SPAM', 0)
        >>> d.register_key('Eggs')
        ('eGgS', 1)
        >>> d.register_key('bacon')
        (None, None)
        """
        # This method is essentially a helper method
        # for the __setitem__ of the CaseInsensitiveDict.
        
        processed_key = self.process_key(unprocessed_key)
        
        try:
            return self[processed_key], self.keys_list.index(processed_key)
        except KeyError as err:
            self[processed_key] = unprocessed_key
            return None, None

    def __setitem__(
        self,
        processed_key: _K,
        unprocessed_key: _K,
        /
    ) -> None:
        # This method is the mirror image of __delitem__
        self.data[processed_key] = unprocessed_key
        self.keys_list.append(processed_key)

    def get_original_key(self, unprocessed_key: _K, /) -> _K:
        """Return the original key, as it was first inputted into the mapping.

        Parameters
        ----------
        unprocessed_key, _K:
            A key that has not yet been normalised
            according to this class's process_key method.

        Return
        ------
        original_key, _K:
            The equivalent key to the inputted value,
            as it was originally inputted into the mapping.

        Raises
        ------
        KeyError if no equivalent
        for the inputted key exists in this mapping.

        Example
        -------
        >>> d = _KeyMapperDict({'spam': 'SPAM', 'eggs': 'eGgS'})
        >>> d.get_original_key('SPAM')
        'SPAM'
        >>> d.get_original_key('Eggs')
        'eGgS'
        >>> d.get_original_key('bacon')
        Traceback (most recent call last):
        KeyError: 'bacon'
        """
        return self[self.process_key(unprocessed_key)]
        
    def __getitem__(self, processed_key: _K, /) -> _K:
        return self.data[processed_key]

    def remove_key(self, unprocessed_key: _K, /) -> int:
        """Delete the key from the mapping, return the index where the key used to be.

        Example
        -------
        >>> d = _KeyMapperDict({'spam': 'SPAM', 'eggs': 'eGgS', 'bacon': 'bacon'})
        >>> d.remove_key('EGGS')
        1
        >>> d
        _KeyMapperDict({'spam': 'SPAM', 'bacon': 'bacon'})
        >>> d.remove_key('Spam')
        0
        >>> d
        _KeyMapperDict({'bacon': 'bacon'})
        """
        
        # This method is essentially a helper method
        # for the __delitem__ of the CaseInsensitiveDict
        processed_key = self.process_key(unprocessed_key)
        index = self.keys_list.index(processed_key)
        del self[processed_key]
        return index
        
    def __delitem__(self, processed_key: _K, /) -> None:
        # This method is the mirror image of __setitem__
        del self.data[processed_key]
        self.keys_list.remove(processed_key)

    def index_of_key(self, unprocessed_key: _K, /) -> Optional[int]:
        """Return the "integer index" of a certain key in the keys_list.

        Parameters
        ----------
        unprocessed_key, _K:
            A key that may or may not be in the keys_list,
            and may or may not be of the same case as it was
            originally entered into the mapping.

        Return
        ------
        index, int or None:
            Either the integer index of the key in the keys_list,
            or None if the key is not in the keys_list.

        Example
        -------
        >>> d = _KeyMapperDict({'spam': 'SPAM', 'eggs': 'eGgS'})
        >>> d.index_of_key('SPAM')
        0
        >>> d.index_of_key('spam')
        0
        >>> d.index_of_key('eggs')
        1
        >>> d.index_of_key('bacon') is None
        True
        """
        processed_key = self.process_key(unprocessed_key)

        try:
            return self.keys_list.index(processed_key)
        except ValueError:
            return None

    def original_key_at_index(self, index: int, /) -> Optional[_K]:
        """Return the key at a certain integer index in the keys_list.

        Return the key as it was originally entered into the mapping,
        rather than the normalised version of the key.
        
        The keys as they were originally entered
        into the CaseInsensitiveDict
        are stored as this mapping's values.


        Parameters
        ----------
        index, int:
            An index that may or may not be valid.

        Return
        ------
        key, _K or None:
            Either the object at that integer index in the keys_list,
            or None if the integer index isn't valid.

        Example
        -------
        >>> d = _KeyMapperDict({'spam': 'SPAM', 'eggs': 'eGgS'})
        >>> d.original_key_at_index(0)
        'SPAM'
        >>> d.original_key_at_index(1)
        'eGgS'
        >>> d.original_key_at_index(5) is None
        True
        """
        try:
            return self.data[self.keys_list[index]]
        except IndexError:
            return None


class CaseInsensitiveDict(_DictBoilerplateBase[_K, _V]):
    """A case-insensitive dict.

    Where `c = CaseInsensitiveDict()`,
    `c['Adele']` returns the same value as `c['adele']` or `c['AdElE']`.
    The dict also records the integer index of the keys and values.

    The dict preserves the original case
    of the first time the key was entered into the mapping.
    E.g., it will remember that "Adele"
    as first entered into the mapping as "Adele",
    even if the value associated with "Adele" in the mapping
    is updated using the code `c['adele'] = '21'`.
    """

    __slots__ = 'keys_map', 'values_list'
    
    def __init__(
        self,
        data: Optional[Mapping[_K, _V]] = None,
        /,
        **kwargs
    ) -> None:
        
        # This is where the actual data is stored
        # It maps the UNPROCESSED keys to the values
        self.data: dict[_K, _V] = {}

        # A map of PROCESSED keys to the ORIGINAL keys
        self.keys_map: _KeyMapperDict[_K] = _KeyMapperDict()

        # A list of the key values, for integer indexing
        self.values_list: list[_V] = []

        super().__init__(data, **kwargs)

    def __setitem__(self, unprocessed_key: _K, value: _V, /) -> None:
        # (original_key, index) will be (_K, int)
        # if the key's already in the mapping.
        # Else (None, None)        
        original_key, index = self.keys_map.register_key(unprocessed_key)

        # Better to check the index,
        # as None can plausibly be used as a dictionary key
        if index is None:
            self.values_list.append(value)
            self.data[unprocessed_key] = value
        else:
            self.values_list[index] = value
            self.data[original_key] = value  # type: ignore[index]
        
    def __getitem__(self, unprocessed_key: _K, /) -> _V:
        # Catch KeyErrors and reraise them,
        # to make for a more logical traceback.
        try:
            processed_key = self.keys_map.get_original_key(unprocessed_key)
        except KeyError as err:
            raise KeyError(*err.args) from err
        else:
            return self.data[processed_key] 
        
    def __delitem__(self, unprocessed_key: _K, /) -> None:
        # Catch KeyErrors and reraise them,
        # to make for a more logical traceback.
        try:
            original_key = self.keys_map.get_original_key(unprocessed_key)
        except KeyError as err:
            raise KeyError(*err.args) from err
        else:
            del self.data[original_key]
            # the original_key is not processed -
            # the _KeyMapperDict does that for us
            self.values_list.pop(self.keys_map.remove_key(original_key))

    def index_of_key(self, unprocessed_key: _K, /) -> Optional[int]:
        """Return the integer index of a certain key in the mapping.

        This method is effectively delegated
        to the instance's `keys_map` attribute,
        which is of type `_KeyMapperDict`.

        Parameters
        ----------
        unprocessed_key, _K:
            A key that may or may not be in the mapping.

        Return
        ------
        index, int or None:
            Either the integer index of the key in the mapping,
            or None if the key is not in the mapping.

        Example
        -------
        >>> d = CaseInsensitiveDict({'a': 'spam', 'b': 'eggs'})
        >>> d.index_of_key('A')
        0
        >>> d.index_of_key('a')
        0
        >>> d.index_of_key('b')
        1
        >>> d.index_of_key('c') is None
        True
        """
        return self.keys_map.index_of_key(unprocessed_key)

    def index_of_value(self, *args: Any, **kwargs: Any) -> NoReturn:
        """Raise NotImplementedError.

        Example
        -------
        >>> d = d = CaseInsensitiveDict({'a': 'spam', 'b': 'eggs'})
        >>> d.index_of_value('spam')
        Traceback (most recent call last):
        NotImplementedError: index_of_value \
is deliberately not implemented as a method. \
Multiple values can exist in a dictionary \
that are all the same, therefore it does not make sense \
to request the index of a dictionary value.
        """
        raise NotImplementedError(
            "index_of_value is deliberately not implemented as a method. "
            "Multiple values can exist in a dictionary that are all the same, "
            "therefore it does not make sense "
            "to request the index of a dictionary value."
        )

    def index_of_item(self, *args: Any, **kwargs: Any) -> NoReturn:
        """Raise NotImplementedError.

        Example
        -------
        >>> d = d = CaseInsensitiveDict({'a': 'spam', 'b': 'eggs'})
        
        >>> d.index_of_item('a', 'spam')
        Traceback (most recent call last):
        NotImplementedError: index_of_item is deliberately not implemented. \
Use index_of_key instead.

        >>> d.index_of_item(('a', 'spam'))
        Traceback (most recent call last):
        NotImplementedError: index_of_item is deliberately not implemented. \
Use index_of_key instead.

        """
        raise NotImplementedError(
            "index_of_item is deliberately not implemented. "
            "Use index_of_key instead."
        )

    def key_at_index(self, index: int, /) -> Optional[_K]:
        """Return the key at a certain integer index in the mapping.

        Return the key as it was originally entered into the mapping,
        not the normalised version of the key.

        This method is effectively delegated
        to the instance's `keys_map` attribute,
        which is of type `_KeyMapperDict`.

        Parameters
        ----------
        index, int:
            An index that may or may not be valid.

        Return
        ------
        key, _K or None:
            Either the key at that integer index in the mapping,
            or None if the integer index isn't valid.

        Example
        -------
        >>> d = CaseInsensitiveDict({'a': 'spam', 'b': 'eggs'})
        >>> d.key_at_index(0)
        'a'
        >>> d.key_at_index(1)
        'b'
        >>> d.key_at_index(2) is None
        True
        """
        return self.keys_map.original_key_at_index(index)

    def value_at_index(self, index: int, /) -> Optional[_V]:
        """Return the value at a certain integer index in the mapping.

        Parameters
        ----------
        index, int:
            An index that may or may not be valid.

        Return
        ------
        value, _V or None:
            Either the value at that integer index in the mapping,
            or None if the integer index isn't valid.

        Example
        -------
        >>> d = CaseInsensitiveDict({'a': 'spam', 'b': 'eggs'})
        >>> d.value_at_index(0)
        'spam'
        >>> d.value_at_index(1)
        'eggs'
        >>> d.value_at_index(5) is None
        True
        """
        try:
            return self.values_list[index]
        except IndexError:
            return None

    def item_at_index(self, index: int, /) -> Optional[tuple[_K, _V]]:
        """Return the (key, value) pair at a certain integer index in the mapping.

        Parameters
        ----------
        index, int:
            An index that may or may not be valid.

        Return
        ------
        item, (_K, _V) or None:
            Either the (key, value) pair
            at that integer index in the mapping,
            or None if the integer index isn't valid.

        Example
        -------
        >>> d = CaseInsensitiveDict({'a': 'spam', 'b': 'eggs'})
        >>> d.item_at_index(0)
        ('a', 'spam')
        >>> d.item_at_index(1)
        ('b', 'eggs')
        >>> d.item_at_index(2) is None
        True
        """
        key = self.key_at_index(index)
        return None if key is None else (key, self[key])

    def multiget(
        self,
        /,
        *key_sequence: _K,
        default: Optional[_D] = None
    ) -> list[Union[_V, _D, None]]:
        """Return a list of values corresponding to an arbitrary sequence of keys.

        If any item in key_sequence is not a valid key,
        the default value goes in the list.

        Example
        -------
        >>> d = CaseInsensitiveDict({'a': 1, 'c': 2})
        >>> d.multiget('a', 'b', 'c', default='spam')
        [1, 'spam', 2]
        >>> d
        CaseInsensitiveDict({'a': 1, 'c': 2})
        """
        # The type-hint for the default is _D rather than _V,
        # as dict.get() does not update the mapping at all.
        # Therefore the default does not need to be the same type
        # as the mapping's values.
        return [self.get(k, default) for k in key_sequence]

    def multipop(self, /, *key_sequence: _K) -> list[_V]:
        """Return a list of values corresponding to an arbitrary sequence of keys.
        Remove said keys from the mapping.

        Example
        -------
        >>> d = CaseInsensitiveDict({'a': 1, 'c': 2})
        >>> d.multipop('a', 'c')
        [1, 2]
        >>> d
        CaseInsensitiveDict({})
        """
        return [self.pop(k) for k in key_sequence]

    def multipopitem(self, n: int, /) -> list[tuple[_K, _V]]:
        """Return a list, of length `n`, of (key, value) pairs, popped from the beginning of the mapping.

        NOTE: classes inheriting from collections.abc.MutableMapping
        pop mapping items in "first-in-first-out" (FIFO) order.
        This is the opposite order to python's builtin `dict`,
        `collections.OrderedDict` and `collections.Counter`,
        all of which use "last-in-first-out" (LIFO).

        Example
        -------
        >>> d = CaseInsensitiveDict({'a': 1, 'b': 2, 'c': 3, 'd': 4})
        >>> d.multipopitem(3)
        [('a', 1), ('b', 2), ('c', 3)]
        >>> d
        CaseInsensitiveDict({'d': 4})
        """
        return [self.popitem() for _ in range(n)]

    def multisetdefault(
        self,
        /,
        *key_sequence: _K,
        default: Optional[_V] = None
    ) -> list[Optional[_V]]:
        """Return a list of values corresponding to an arbitrary sequence of keys.

        If any item in key_sequence is not a valid key,
        the default value goes in the list.

        Additionally, the mapping will be updated
        such that the key is added to the mapping,
        with this function's `default` parameter
        as the associated value.

        Example
        -------
        >>> d = CaseInsensitiveDict({'a': 1, 'c': 2})
        >>> d.multisetdefault('a', 'b', 'c', default='spam')
        [1, 'spam', 2]
        >>> d
        CaseInsensitiveDict({'a': 1, 'c': 2, 'b': 'spam'})
        """
        return [self.setdefault(k, default) for k in key_sequence] # type: ignore[arg-type]
Find elsewhere
🌐
Mathspp
mathspp.com › blog › how-to-work-with-case-insensitive-strings
How to work with case-insensitive strings | mathspp
January 21, 2023 - To implement a case-insensitive dictionary in Python, we need to use the string method str.casefold whenever we are setting, getting, or deleting a key from the dictionary.
🌐
GitHub
gist.github.com › babakness › 3901174
A Python dictionary sub-class that is case-insensitive when searching, but also preserves the keys as inserted. · GitHub
A Python dictionary sub-class that is case-insensitive when searching, but also preserves the keys as inserted. - caselessDictionary.py
🌐
PyPI
pypi.org › project › nocasedict
nocasedict · PyPI
Class NocaseDict is a case-insensitive ordered dictionary that preserves the original lexical case of its keys. ... $ python >>> from nocasedict import NocaseDict >>> dict1 = NocaseDict({'Alpha': 1, 'Beta': 2}) >>> dict1['ALPHA'] # Lookup by ...
      » pip install nocasedict
    
Published: Jan 04, 2026
Version: 2.2.0
🌐
PyPI
pypi.org › project › caseless-dictionary
caseless-dictionary · PyPI
A simple, fast, typed, and tested implementation for a python3.6+ case-insensitive and attribute case-insensitive dictionaries.
🌐
Python
bugs.python.org › issue18986
Issue 18986: Add a case-insensitive case-preserving dict - Python tracker
September 9, 2013 - This issue tracker has been migrated to GitHub, and is currently read-only. For more information, see the GitHub FAQs in the Python's Developer Guide · This issue has been migrated to GitHub: https://github.com/python/cpython/issues/63186
🌐
ActiveState
code.activestate.com › recipes › 66315-case-insensitive-dictionary
Case-insensitive Dictionary « Python recipes « ActiveState Code
# ******************************************** # class caselessDict # purpose emulate a normal Python dictionary # but with keys which can accept the # lower() method (typically strings). # Accesses to the dictionary are # case-insensitive but keys returned # from the dictionary are always in # the original case.
🌐
Reddit
reddit.com › r/cs50 › dictionary, keys, casefold()
r/cs50 on Reddit: Dictionary, Keys, casefold()
December 29, 2023 -

Hello guys,

There is a problem with dictionary keys needing to be case-insensitive in week 2. I just solved it by using casefold() in function parameter because I don't know how to use casefold() in dictionary keys. Now, I encounter the same problem in problem set 3. So I am just wondering if there is a way to use casefold() in dictionary keys.

Thank you very much.

🌐
Bytes
bytes.com › home › forum › topic › python
Case insensitive dictionary? - Python - Bytes
If so what is?[/color] I'm probably a bit late on this one... but I've written a complete case-insensitive dictionary (and list..) with all dictionary methods implemented.... It's called caseless and available at : http://www.voidspace.org.uk/atlantibots/pythonutils.html#configobj
🌐
GitHub
github.com › DeveloperRSquared › case-insensitive-dict
GitHub - DeveloperRSquared/case-insensitive-dict: Typed Python Case Insensitive Dictionary · GitHub
Returns the original case-sensitive key using a case-insensitive search. ... Returns a dictionary with the specified keys and the specified value.
Starred by 5 users
Forked by 3 users
Languages: Python
🌐
sqlpey
sqlpey.com › python › case-insensitive-dictionary
Top 4 Methods to Create a Case Insensitive Dictionary in Python
November 23, 2024 - A tidy solution is to create a new dictionary where all keys are stored in lower case, while mapping them to their corresponding values. Here is the revised approach: text = "practice changing the Color" words = {'color': 'colour', 'practice': 'practise'} def case_insensitive_replace(words, ...