# django-bootstrap3 and the cost of staying on Bootstrap 3

> django-bootstrap3 renders Django forms as Bootstrap 3 markup through template tags, and its author froze it to bug fixes after Bootstrap 5 arrived. The useful part for existing code is the tag library and the example app; the part that decides a new project is the dependency floor in pyproject.toml, which is still moving.

**zostera/django-bootstrap3** — Bootstrap 3 integration with Django.

- Repository: https://github.com/zostera/django-bootstrap3
- Website: http://django-bootstrap3.readthedocs.org/
- Stars: 2,325 · Forks: 669
- Language: Python
- License: BSD-3-Clause
- Published: 2026-10-14 · Updated: 2026-10-14 · Language: en
- Canonical page: https://hysenlabs.com/projects/zostera-django-bootstrap3

## Django form markup without hand-written Bootstrap 3 classes

A Django form reaches a template as a set of bare fields, and the page author attaches CSS framework classes by hand, which is exactly the work that goes stale whenever the design system moves. django-bootstrap3 removes that work with a template tag library that reads a form object and emits Bootstrap 3 markup.

Loading the library and handing over a form takes two tags:
```djangotemplate
{% load bootstrap3 %}

{# Display a form #}

<form action="/url/to/submit/" method="post" class="form">
    {% csrf_token %}
    {% bootstrap_form form %}
    {% buttons %}
        <button type="submit" class="btn btn-primary">Submit</button>
    {% endbuttons %}
</form>
```
Everything around those tags stays yours. The example writes the form element by hand, carries `class="form"` on it, and keeps `{% csrf_token %}` between the opening tag and the rendered fields. Buttons are handled separately again: `{% buttons %}` and `{% endbuttons %}` wrap the row that holds the submit control, so a template can restyle the button without touching field rendering.

That split is the real mechanism, and it sets a boundary. The package renders fields, not pages. Nothing in the installation steps loads a stylesheet or a script bundle for you, and no base template appears in the example, so the markup it emits is only half of a Bootstrap 3 page.

## The app string is bootstrap3, not the distribution name

Two different names are in play, and the README keeps them apart. The distribution you install is django-bootstrap3. What you register in settings is shorter:
```python
INSTALLED_APPS = (
    # ...
    "bootstrap3",
    # ...
)
```
The template layer uses that same short name, which is why the load tag reads `{% load bootstrap3 %}` and not something carrying the full distribution name. Nothing in the README says what a mixed-up pair of names does at startup, so treat the short string as load-bearing rather than cosmetic.

Once both names line up, `{% bootstrap_form form %}` does the work a bare form variable leaves undone: labels, help text, error markup and the form-group structure Bootstrap 3 expects, for bound and unbound forms alike. The tag takes the form object as its argument, which keeps it usable in any template that has one in scope.

No settings key appears anywhere in the README for a theme, a variant, or a custom template path. Every visual decision past the point where the tags stop emitting HTML belongs to your own template files.

## Two toolchains: pip for users, uv and just for maintainers

For anyone consuming the package, installation is one command:
```bash
pip install django-bootstrap3
```
A checkout is the documented alternative, where `pip install -e .` installs the project in editable mode. Every other task in the repository runs on a different toolchain. pyproject.toml names uv_build as its build backend, and the justfile drives development through uv, whose default recipe prints the command list and then exits with an error when uv is missing from PATH.

For a contributor the recipes are these:
```bash
uv sync --frozen --all-groups
uv run --no-sync manage.py test
uvx --with tox-uv tox
```
The first resolves strictly from uv.lock instead of re-resolving, the second runs the Django test command directly, and the third hands the matrix to tox. The example app sits under `example/` with its own `example/settings.py` and `example/manage.py`, and `just example` starts it. The justfile also defines `EXAMPLE_DIRNAME := "example"` and aliases `tox` to the tests recipe, so the same work is reachable under two command names.

A reader with only pip installed can follow the README end to end. A contributor without uv and just cannot run any of the repository's own recipes.

## Feature freeze, rising dependency floor, one stale classifier

Direction is not in doubt. The README states that Bootstrap 3 has been superseded by Bootstrap 4, which has since been superseded by Bootstrap 5, and that the package now takes bug fixes and security updates with no new features or enhancements. New projects are pointed at Bootstrap 5, existing ones are asked to migrate when feasible.

What the packaging metadata shows is narrower than that paragraph. The freeze covers the Bootstrap layer and the feature list, not the platforms underneath:
```toml
dependencies = ["Django>=5.2"]
requires-python = ">=3.10"
version = "26.2"
```
The classifier list reaches Django 6.0 and 6.1 and Python 3.14, well past the floor the dependencies declare. Sitting beside them, `Development Status :: 5 - Production/Stable` calls the package stable, which reads oddly next to a notice that forbids new features. The version string, 26.2, follows a numbering scheme that nothing in the README unpacks.

The package ships under BSD-3-Clause with LICENSE and AUTHORS listed as licence files, and the repository carries no GitHub release page, so the change history sits in the CHANGELOG.md file instead. The last push landed on 2026-09-21.

## A Trojan Source guard sits inside the lint recipe

Linting here is four commands, and the fourth is not about style:
```bash
uvx ruff format --check
uvx ruff check
uvx typos
just _no-invisible-characters
```
The private recipe behind that last name fails the build when any tracked file holds an invisible or bidirectional control character. Its comment names the reason plainly: those characters stay invisible in review, which is what makes them useful for hiding code, and it cites Trojan Source, CVE-2021-42574.

The scan is a `git grep -nIP` over a character class covering the soft hyphen, the zero width space and its siblings, the directional formatting range, the isolate marks and the byte order mark. A repository whose payload is Python plus Django templates has little exposure to that class of bug in its own source, yet the check costs one command to run and nothing to maintain.

Ruff handles the rest of the hygiene. `fix = false` in pyproject.toml keeps lint runs read-only, `line-length = 120` sets the width, and `src = ["src"]` tells the linter where the package lives. The fixable set covers sorting, flake8, docformatter and pyupgrade, while D1 and D203 sit in the ignore list because not every function carries a docstring.

## Questions the README leaves open for adopters

The requirements section is the thinnest part of the documentation. It asks for a combination of Python and Django that is currently supported and then points at the Supported Versions table on djangoproject.com, a page that shifts under the reader's feet. The exact statement lives in pyproject.toml instead, and nothing joins the two.

Four questions a new adopter will ask get no answer there. How assets reach the page is not documented: the install steps end at INSTALLED_APPS, and no snippet loads a stylesheet or a script bundle. Icon markup is routed elsewhere, to django-icons, which the README recommends as a dedicated package. There is no settings key for a theme or a variant, and no override point for a custom template. Nor is there a rollback path described for the case where a Django upgrade turns a form tag into an error page.

None of that makes the package brittle. It makes the README a starting point rather than a reference, with pyproject.toml, the justfile and the docs/ directory carrying the detail.

## django-bootstrap4 and django-bootstrap5 are the stated destinations

A package frozen on one Bootstrap generation needs somewhere to point, and the README names both destinations: django-bootstrap4 for Bootstrap 4 and django-bootstrap5 for Bootstrap 5, each maintained by the same author. Its recommendation is blunt about which one to pick. New projects should use Bootstrap 5, and existing ones should consider migrating when feasible.

That is the whole of the migration guidance, and the difference in approach is visible only by running the other package against your templates. No upgrade note describes what changes in the tag API between generations, no compatibility shim is documented, and the differences in generated markup are left for a reader to discover. What continuity exists is structural: same author, same packaging layout, same template tag approach, so the mental model transfers even when the output does not.

A team with Bootstrap 3 markup and no Bootstrap 5 templates anywhere is the audience that should stay and keep patching. A team with a Bootstrap 5 design already in flight should stop reading about this package and read the other one.

## Conclusion

Adopt django-bootstrap3 only for a codebase that already renders Bootstrap 3 and has no schedule to reach Bootstrap 5; anything new started on it inherits a front end its own author has retired. Leave it alone if your templates already carry Bootstrap 4 or 5 markup. Before committing, check that pip install django-bootstrap3 resolves against your Django version, then render one real form with a validation error after adding "bootstrap3" to INSTALLED_APPS and {% load bootstrap3 %}, and read the coverage report from the justfile's test recipe.

## FAQ

### Is django-bootstrap3 still actively developed?

No. The README places it in maintenance mode, where it receives bug fixes and security updates with no new features or enhancements, and it directs new projects to Bootstrap 5. The last push was on 2026-09-21.

### How do I install django-bootstrap3 in a Django project?

Run pip install django-bootstrap3, add "bootstrap3" to INSTALLED_APPS in settings.py, and load the library in each template with {% load bootstrap3 %}. A checkout can be installed instead with pip install -e .

### Which Python and Django versions does django-bootstrap3 support?

pyproject.toml declares requires-python of 3.10 and above and depends on Django 5.2 and above, while the classifier list names Django 5.2, 6.0 and 6.1 with Python 3.10 through 3.14. The project version is 26.2.

### Does django-bootstrap3 ship Bootstrap 3 icons and CSS?

The README documents no asset loading step and no icon markup for this package. For icons it recommends a separate package, django-icons, and the installation steps stop at INSTALLED_APPS.

### How do I run the django-bootstrap3 test suite?

The justfile wires tests through uv. One recipe runs coverage run manage.py test followed by a coverage report, another runs manage.py test on its own, and a third invokes tox through uvx --with tox-uv tox for the wider matrix.

## Sources

- [Issues](https://github.com/zostera/django-bootstrap3/issues)
- [License: BSD-3-Clause](https://github.com/zostera/django-bootstrap3/blob/main/LICENSE)
- [Project website](http://django-bootstrap3.readthedocs.org/)
- [README](https://github.com/zostera/django-bootstrap3/blob/main/README.md)
- [zostera/django-bootstrap3 on GitHub](https://github.com/zostera/django-bootstrap3)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/zostera-django-bootstrap3
