Skip to main content

django-select-multiple-field

Store multiple choices in a single Django model field without using a many-to-many relationship.

Rendered using the multiselect.js plugin for jQuery

Rendered using the multiselect.js plugin for jQuery. The plugin is available here: https://github.com/lou/multi-select

Denormalization Risk (Important)

This field intentionally denormalizes data by storing multiple selected choices as comma-separated text in a single column.

Use it when:

  • you want a simple schema,
  • you do not need relational queries (joins),
  • and you mainly read/write the selected values as a list in application code.

If you need robust querying like “find all cookies with topping X,” a normalized many-to-many model is usually the better choice.

For example, if cookies can have toppings such as chocolate, dark_chocolate, and white_chocolate, a normalized design would usually use a Topping model and a Cookie model related by a ManyToManyField. That gives you joins, indexing, and exact filtering.

With this field, you filter against the stored text value instead.

# Caution: substring search can return false positives.
Cookie.objects.filter(toppings__icontains="chocolate")

That kind of lookup can produce false positives because one encoded choice can appear inside another. For example, chocolate is a substring of dark_chocolate, so a substring search can match both.

If that risk is unacceptable for your application, use a many-to-many relation instead.

Installation

Install from PyPI:

pip install django-select-multiple-field

Quick Start

Model

You must provide either max_length or both choices and max_choices

Choices must be strings, not integers.

Add the select field choices normally in your model:

# models.py

from django.db import models

from select_multiple_field.models import SelectMultipleField

class Pizza(models.Model):
    ANCHOVIES = 'a'
    BLACK_OLIVES = 'b'
    PEPPERONI = 'p'
    MOZZARELLA = 'm'
    TOPPING_CHOICES = (
        (ANCHOVIES, 'Anchovies'),
        (BLACK_OLIVES, 'Black olives'),
        (PEPPERONI, 'Pepperoni'),
        (MOZZARELLA, 'Mozzarella'),
    )
    # choices & max_choices are used to auto calc. max_length
    toppings = SelectMultipleField(
        max_choices=4,
        choices=TOPPING_CHOICES,
    )

    PLAIN = 'n'
    CHEDDAR = 'c'
    CRUST_CHOICES = (
        (PLAIN, 'Plain Crust'),
        (MOZZARELLA, 'Mozzarella Stuffed Crust'),
        (CHEDDAR, 'Cheddar Stuffed Crust'),
    )
    crust = SelectMultipleField(
        max_length=6,
        choices=CRUST_CHOICES,
    )

Encoded Length (Choose One)

Underneath this field is a CharField which takes a maximum length for the encoded string. You can choose to provide a max_length parameter or the field will calculate it from the choices and the max_choices parameters.

  • max_length is the maximum length of the encoded string of choices. You need to add the maximum allowed choices and the delimiter character (usually a comma) that separates the choices when they are encoded.
  • max_choices is the maximum choices that can be encoded per field.
  • choices can be a mapping, iterable, or callable.

Example encoded-length calculation:

  • Choice keys: a, bb, ccc, dddd
  • max_choices=2
  • Longest 2 keys are dddd and ccc, so encoded value is dddd,ccc
  • Required max_length is 8

Render the form

Use a generic view or a ModelForm as usual. In your template, use a regular form tag:

<!-- template_form.html -->
<form action="" method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <input type="submit" value="Submit">
</form>

This renders the following HTML:

<!-- create.html -->
<form action="" method="post">
    <p>
      <label for="id_toppings">Toppings:</label>
      <select multiple="multiple" id="id_toppings" name="toppings" class="select-multiple-field">
        <option value="a">Anchovies</option>
        <option value="b">Black olives</option>
        <option value="p">Pepperoni</option>
        <option value="m">Mozzarella</option>
      </select>
    </p>
    <input type="submit" value="Submit">
</form>

Null Handling

SelectMultipleField supports null=True, but it behaves differently from Django's standard CharField:

null Empty Selection in DB to_python(None) from_db_value(None) get_prep_value([])
False (default) "" (empty string) [] [] ""
True NULL [] [] None

Key semantics:

  • null=True changes only database storage. Python API always returns [], never None
  • to_python(None) always returns [] regardless of null setting
  • from_db_value(None, ...) always returns [] regardless of null setting
  • get_prep_value(None) returns None when null=True, else ""
  • get_prep_value([]) returns None when null=True, else ""

Use null=True only when your database conventions require NULL over empty string for empty selections. For form-level optional fields, use blank=True instead.

Validators

SelectMultipleField replaces Django's built-in MaxLengthValidator with two custom validators:

MaxChoicesValidator

  • Validates: len(value) ≤ max_choices
  • Error code: max_choices
  • Message: "Ensure this value has at most %(limit_value)d choice(s)..."

MaxLengthValidator

  • Validates: len(encode_list_to_csv(value)) ≤ max_length
  • Error code: max_length
  • Message: "Ensure this value has at most %(limit_value)d character(s)..."

Both validators run during model validation. If you provide choices and max_choices but omit max_length, the field auto-calculates max_length from the longest possible encoded CSV (longest max_choices keys joined by delimiters). If you explicitly set max_length smaller than this calculated maximum, a RuntimeWarning is emitted at field instantiation.

Validation Error Codes

The field can raise the following validation errors:

Error Code Message Template Triggered When
invalid_type "Types passed as value must be string, list, tuple or None, not '%(value)s'." to_python receives non-list/tuple/str/None
invalid_choice "Select a valid choice. %(value)s is not one of the available choices." Value not in choices
blank "This field cannot be blank." blank=False and empty value
null "This field cannot be null." null=False and None value
max_choices "Ensure this value has at most %(limit_value)d choice(s)..." len(value) > max_choices
max_length "Ensure this value has at most %(limit_value)d character(s)..." len(encoded) > max_length

Displaying Stored Choices

To display your choices, decode the field contents. You can do this with a template tag:

# templatetags/pizza_tags.py

def decode_pie(ingredients):
    """Decode pizza pie toppings."""
    decoder = dict(Pizza.TOPPING_CHOICES)
    decoded = [decoder[t] for t in ingredients]
    decoded.sort()
    return ', '.join(decoded)

register.filter('decode_pie', decode_pie)

In your template, import the tag and use it:

{# details.html #}
{% load pizza_tags %}

{{ pizza.toppings|decode_pie }}

Encoding the Choices

The selected choices are stored as comma-delimited text. For example, a pizza with the following toppings:

  • Pepperoni
  • Mozzarella

would be stored as:

p,m

You can decode that string to a Python list using functions in the codecs module:

>>> from select_multiple_field.codecs import decode_csv_to_list
>>> encoded = 'a,b,c'
>>> decoded = decode_csv_to_list(encoded)
>>> print(decoded)
['a', 'b', 'c']
>>> print(type(decoded))
<class 'list'>

Custom Delimiters

The CSV delimiter is configurable via Django settings:

# settings.py
SELECTMULTIPLEFIELD_DELIMITER = "|"  # Default: ","

Constraints:

  • Must be a single character
  • Multi-character delimiters are not supported
  • Choice values cannot contain the delimiter (e.g., if delimiter is ,, a choice like "a,b" breaks encoding)

Both encode_list_to_csv() and decode_csv_to_list() in select_multiple_field.codecs respect this setting automatically. Changing the delimiter on an existing database requires a data migration to re-encode stored values.

The encoding method may limit your ability to search for choices.

Sample Application

This repository includes sample applications under test_projects/. You can run the Django 4.2 integration app like this:

$ cd /path/to/django-select-multiple-field
$ cd test_projects/django42
$ python manage.py migrate
$ python manage.py runserver
Development server is running at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Versions

This code was tested with the following versions of Django and Python:

  • Django 4.2 LTS
    • Python 3.10
    • Python 3.11
    • Python 3.12
    • Python 3.13
  • Django 5.2 LTS
    • Python 3.10
    • Python 3.11
    • Python 3.12
    • Python 3.13
    • Python 3.14
  • Django 6.0
    • Python 3.12
    • Python 3.13
    • Python 3.14

Testing

Django-select-multiple-field contains two test suites: one for the field itself and one for the Django integration apps.

To run the unit tests use hatch interactively:

hatch run tests  # All tests {unit, integration}
hatch run i  # Integration tests
hatch run t  # Unit tests

The dependencies are managed manually in your environment.

To run all the tests with differing Python versions and Django versions, use tox:

tox  # All tests for supported versions
tox -e py312-dj42  # All tests using Python 3.12 & Django 4.2 LTS

Bugs! Help!!

If you find any bugs in this software, please report them via the GitHub issue tracker or send an email to code@kelvinwong.ca. Any serious security bugs should be reported via email only.

Issue tracker: https://github.com/kelvinwong-ca/django-select-multiple-field/issues

Links

Thank You

Thank you for taking the time to evaluate this software. I appreciate receiving feedback on your experiences using it, and I welcome code contributions and development ideas.

http://www.kelvinwong.ca/coders

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_select_multiple_field-1.0.0.tar.gz (12.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_select_multiple_field-1.0.0-py3-none-any.whl (14.7 kB view details)

Uploaded Python 3

File details

Details for the file django_select_multiple_field-1.0.0.tar.gz.

File metadata

  • Download URL: django_select_multiple_field-1.0.0.tar.gz
  • Upload date:
  • Size: 12.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: Hatch/1.18.0 {"ci":null,"cpu":"arm64","distro":{"name":"macOS","version":"15.7.9"},"implementation":{"name":"CPython","version":"3.12.13"},"installer":{"name":"hatch","version":"1.18.0"},"openssl_version":"OpenSSL 3.6.3 9 Jun 2026","python":"3.12.13","system":{"name":"Darwin","release":"24.6.0"}} HTTPX2/2.12.0

File hashes

Hashes for django_select_multiple_field-1.0.0.tar.gz
Algorithm Hash digest
SHA256 ac368fe7dd538936818a0a536fcf645adfa5dafc6cba9257df2d2b8c60fa36ae
MD5 661c5b6123674068a17b74e5dd5e32e1
BLAKE2b-256 32cdb10f3c5a7626e98ddd96700b94954724417e99772c8acab4125d73133659

See more details on using hashes here.

File details

Details for the file django_select_multiple_field-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: django_select_multiple_field-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 14.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: Hatch/1.18.0 {"ci":null,"cpu":"arm64","distro":{"name":"macOS","version":"15.7.9"},"implementation":{"name":"CPython","version":"3.12.13"},"installer":{"name":"hatch","version":"1.18.0"},"openssl_version":"OpenSSL 3.6.3 9 Jun 2026","python":"3.12.13","system":{"name":"Darwin","release":"24.6.0"}} HTTPX2/2.12.0

File hashes

Hashes for django_select_multiple_field-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6c7f41a31309982b9085129454faf345143093dfdc0daca2ecdadcea25df291c
MD5 f7bf829b65e1b61adaa1d69408eb4ef4
BLAKE2b-256 1b7b2f8708059190634723a87980f60180629724d90a0d20b0c17455244bc497

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 files

0.4.2

1 file

0.4.1

1 file

0.4.0

1 file

0.3.1

1 file

0.3.0

1 file

0.2.0

1 file

0.1.0

1 file

0.0.5

1 file

0.0.4

1 file

0.0.3

1 file

0.0.2

1 file

0.0.1

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page