Django 环境搭建与版本迁移

这篇讲什么

  • 这是这套 Django 笔记的入口:38 篇文章按目录怎么读、当前基线是怎么定下来的、以及 2019 年那批 1.x 写法在今天哪些还能跑、哪些已经报错。
  • 文末给了一个能整段复制跑起来的最小工程,以及迁移自己项目时的检查顺序。

这套 Django 笔记怎么读

  • 地图如下。篇数是实测统计(ls 数出来的),「本批做了什么」指的是 2026-11-04 这一批只做了结构合并与版本口径统一,逐篇跑实测重写是后面几批的事。

    子目录 篇数 装什么 本批做了什么
    入门 14 基本入门、settings 详解、模型、视图、模板、验证码、站点管理、中间件、上传文件、分页、Ajax、富文本、发送邮件、缓存 url() → re_path()、外键补 on_delete、is_ajax() 换成请求头判断、删 updated:、补 tags;settings 详解
    REST框架 8 理解 RESTful、设计风格、序列化、请求与响应、基于类的视图、认证和权限、ViewSets 与 Routers、DRF 架构总结 同上(这批 url() 最密集)
    异步任务与信号 2 Celery(架构与用法,原来两篇已合并)、Signals 同上,另挂 djcelery 停更提示
    常用插件 1 常用插件清单(Xadmin 使用教程已并入《Django-站点管理》) 挂「已停更」提示,现代替代方案见第三批重写;目录 11-05 由「后台与常用插件」改名
    开发方法论 2 快速迭代开发过程、通往 Hacker 之路 纯方法论,无代码,只补 tags
    部署与运维 9 Sentry 架构总结、生产注意事项、水平扩展、让网站访问更快、监控告警、生产安全、云环境部署、持续集成、宝塔 + uWSGI 删 updated:、补 tags、url() 口径统一
  • 想快速上手的话,顺序建议是:本篇(环境和口径)→《Django 5.2 LTS 与 6.x 的区别对比》→ 入门/Django-基本入门.md → 入门/Django-模型.md → 入门/Django-视图.md → 入门/Django-模板.md → 入门/Django-站点管理.md,再按需要进 REST框架 或 部署与运维。

环境与版本口径

  • 版本基线:Django 5.2 LTS,本机实测用的是 5.2.19。选它而不是 6.1 的理由和逐条实测差异都在《Django 5.2 LTS 与 6.x 的区别对比》里,这里只留结论:6.2 LTS(2027-04)发布前不追 6.x,因为 django-celery-beat 这类常用包的上限还卡在 Django<6.1,装了它 pip 会静默把 Django 降级。

  • 实验环境是 conda 里一个独立环境 django,Python 用的是 base 同款的 3.14.10:

    1
    2
    3
    conda create -n django python=3.14 -y
    conda activate django
    python -V # Python 3.14.10
  • 安装走中科大镜像(本机 pip 默认源慢):

    1
    pip install -i https://mirrors.ustc.edu.cn/pypi/web/simple Django==5.2.19
  • 这一批在 5.2.19 环境里实测装上的包与确切版本(pip list + importlib.metadata 逐个数出来的,24 个全部 import 成功,pip check 输出 No broken requirements found.):

    包 版本 装成功 备注
    Django 5.2.19 是 requires_python >= 3.10,LTS 延长支持到 2028-04
    djangorestframework 3.18.3 是 要求 django>=5.2
    django-filter 26.2 是
    drf-spectacular 0.30.0 是 OpenAPI 文档,替代已停更的 drf-yasg
    celery 5.6.3 是
    django-celery-results 2.6.0 是 要求 Django>=3.2.25,无上限
    django-celery-beat 2.9.0 是 元数据写着 Django<6.1,>=2.2,5.2 上正常,6.1 上会拖着你降级
    redis 8.1.0 是
    django-redis 7.0.0 是 要求 Django<7.0,>=5.2
    PyMySQL 1.2.3 是 纯 Python 兼容路线,只有 install_as_MySQLdb() 那种写法才需要它
    mysqlclient 2.3.0 是 官方推荐的 MySQL 驱动,导入名是 MySQLdb;Windows 有 cp314 wheel,wheel 自带的客户端库是 MariaDB Connector/C 3.4.9
    psycopg / psycopg-binary 3.3.8 是 官方推荐的 PostgreSQL 驱动(psycopg 3,psycopg2 已不受支持),连 WSL 里的 PostgreSQL 18.6 实测通过
    cryptography 50.0.2 是
    Pillow 13.0.0 是 ImageField 需要
    gunicorn 26.2.0 是
    uvicorn 0.54.0 是 ASGI
    whitenoise 6.12.0 是 静态资源
    django-debug-toolbar 8.0.0 是 要求 django>=5.2
    django-tinymce 5.0.0 是 富文本
    django-simple-captcha 0.7.0 是 验证码
    django-ratelimit 4.1.0 是 模块名是 django_ratelimit,老笔记里写的 ratelimit 是另一个已死的包
    django-cors-headers 4.9.0 是
    django-unfold 0.110.1 是 现代后台皮肤,本仓默认推荐(Xadmin 已停更,Simple UI 是备选)
    sentry-sdk 2.73.0 是
    simpleui(django-simpleui) 2026.1.13 是 后台皮肤;老笔记记的「源码包缺 README.md 装不上」在 2026-11-05 复测已不成立,详见《Django-常用插件》
    xadmin 0.6.1 否 包还在 PyPI 上,但本机构建时 UnicodeDecodeError: 'gbk' codec can't decode byte 0xa4;用 UTF-8 模式装上后 import xadmin 又炸 NameError: name 'reload' is not defined(Python 2 写法),停在 2018 年
    djcelery — 否 from versions: none,包已经不在 PyPI 上了,改用 django-celery-beat + django-celery-results
    uwsgi 2.0.9 否 Windows 上 setup.py 里用 os.uname,直接 AttributeError: module 'os' has no attribute 'uname';Linux 上线再谈
  • 「静默降级」的日志长这样(这是在 6.1.3 环境里试装 django-celery-beat 时抓的,一行 warning 都没有):

    1
    2
    Successfully installed Django-6.0.10 cron-descriptor-1.4.5
    django-celery-beat-2.9.0 django-timezone-field-7.2.2 python-crontab-3.4.0

    所以装任何 Django 生态包之前,先看它的 Requires-Dist 里 Django 的上限,别等 pip 悄悄换掉你的版本。

Django 的版本线,以及我们为什么停在 5.2 LTS

  • 官方支持表(2026-11-04 从 djangoproject.com/download/ 抓的):

    发行系列 最新发布 主流支持终结 延长支持结束
    5.2 LTS 5.2.19 2025-12-03 2028-04
    6.0 6.0.10 2026-08-04 2027-04
    6.1 6.1.3 2027-04 2027-12
    6.2 LTS 计划 2027-04 发布 2027-12 2030-04
  • 本套笔记的口径:现在按 5.2 LTS 写,2027 年 4 月 6.2 LTS 发布后整体切到 6.2 LTS。理由有三条:

    1. 5.2 的延长支持到 2028-04,6.2 LTS 到 2030-04,两条线接得上,不会出现「正在用的版本刚失去支持」;
    2. 6.0/6.1 不是 LTS,主流支持只有 8 个月左右,笔记里的写法跟着它们改一遍不划算;
    3. 生态兼容性现实:django-celery-beat 2.9.0 的元数据上限是 Django<6.1。
  • 一个必须知道的规则变化:6.2 是最后一版 A.B 命名、也是最后一次「指定 LTS」。从 Django 2028 起每年 1 月发一版,每版都是三年支持(一年主流 + 两年延长),到时候不用再专门找 LTS 了。

  • Python 侧的约束:5.2 的 requires_python 是 >=3.10,6.0/6.1 是 >=3.12。本机 django 环境是 3.14.10,实测 5.2.19 在 3.14 上 startproject / migrate / check / runserver 全跑通(System check identified no issues (0 silenced).、首页 HTTP 200),官方 FAQ 也把 3.14 列进了 5.2 的支持范围(从 5.2.8 起)。

  • 换版本线时实测到的两个细节:

    • python -m django startproject 生成的 settings.py 用单引号还是双引号,跟 Django 版本无关。真实机制是渲染完模板后 startproject 会调 run_formatters(...),里面 shutil.which("black") 找到了就 black --fast 跑一遍。5.2.19 上亲测:PATH 里有 base 环境那个 black.EXE 时输出 "django.contrib.admin",把 PATH 屏蔽掉就输出 'django.contrib.admin'(模板 settings.py-tpl 本身两版都是单引号)。
    • 所以写脚本批量改 settings 时,替换完必须 assert 期望内容真的进了文件。这个坑我这轮踩过:str.replace 静默不生效,makemigrations 直接报 Model class blog.models.Grade doesn't declare an explicit app_label。

1.x 写法在 5.2.19 上的死活清单

  • 下面每一行都是在 django 环境里 import 一遍得到的真实结果,不是查文档抄的。同一套检查也跑过 6.1.3,18 条结果逐字一致(比对前把报错里各自的安装路径归一化),所以这批替换不随版本基线变化。

    旧写法 在 Django 5.2.19 上的实测结果 现代写法
    from django.conf.urls import url ImportError: cannot import name 'url' from 'django.conf.urls',后面还带一句 Did you mean: 'urls'? from django.urls import path, re_path
    from django.core.urlresolvers import reverse ModuleNotFoundError: No module named 'django.core.urlresolvers' from django.urls import reverse
    from django.utils.translation import ugettext ImportError: cannot import name 'ugettext' from 'django.utils.translation' gettext / gettext_lazy
    from django.utils.encoding import force_text ImportError: cannot import name 'force_text' from 'django.utils.encoding' force_str / smart_str
    from django.utils import six ImportError: cannot import name 'six' from 'django.utils' 删掉,直接 Py3 写法
    @python_2_unicode_compatible ImportError: cannot import name 'python_2_unicode_compatible' 删装饰器,只写 __str__
    from django.shortcuts import render_to_response ImportError: cannot import name 'render_to_response' from 'django.shortcuts' render(request, ...)
    from django.utils.http import urlquote ImportError: cannot import name 'urlquote' from 'django.utils.http' urllib.parse.quote
    from django.contrib.staticfiles.templatetags.staticfiles import static ModuleNotFoundError: No module named 'django.contrib.staticfiles.templatetags' 模板里 {% static %},代码里 django.templatetags.static.static
    Signal(providing_args=[...]) TypeError: Signal.__init__() got an unexpected keyword argument 'providing_args' Signal()
    ForeignKey("Grade") TypeError: ForeignKey.__init__() missing 1 required positional argument: 'on_delete' ForeignKey("Grade", on_delete=models.CASCADE)
    include('myApp.urls', namespace="x") 能 import,但要求被包含的 urls.py 里有 app_name,否则 reverse 找不到 app_name 写在 app 的 urls.py,include() 里不再写 namespace
    request.is_ajax() hasattr(request, 'is_ajax') 为 False request.headers.get("x-requested-with") == "XMLHttpRequest"
    render_to_string('a.html', context, request) 签名是 (template_name, context=None, request=None, using=None),第三个位置参数就是 request 显式写 render_to_string(..., request=request)
  • 最危险的是三类「静默失效」:不报错、check 也全过,但配置根本没生效。5.2.19 实测:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import os
    import django

    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
    django.setup()

    from django.conf import settings
    from django.core.management import call_command

    # 1) 老中间件设置名
    settings.MIDDLEWARE_CLASSES = ["django.middleware.common.CommonMiddleware"]
    call_command("check")
    print("check 没报错;MIDDLEWARE 仍是", len(settings.MIDDLEWARE), "条")

    # 2) 老模板设置名
    settings.TEMPLATE_CONTEXT_PROCESSORS = ("nonexistent.processor",)
    from django.template import engines
    print(engines["django"].engine.context_processors)

    # 3) 老的用户扩展设置名
    settings.AUTH_PROFILE_MODULE = "myapp.Profile"
    from django.contrib.auth import get_user_model
    print(get_user_model())
    1
    2
    3
    4
    System check identified no issues (0 silenced).
    check 没报错;MIDDLEWARE 仍是 7 条
    ['django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages']
    <class 'django.contrib.auth.models.User'>
    • MIDDLEWARE_CLASSES 写了等于没写,check 一声不吭,真正生效的还是 MIDDLEWARE 里那 7 条。
    • TEMPLATE_CONTEXT_PROCESSORS 同理:设一个根本不存在的处理器都不报错,引擎用的还是 TEMPLATES 里那三个;配置要写进 TEMPLATES[...]["OPTIONS"]["context_processors"]。
    • AUTH_PROFILE_MODULE 也一样,设上之后 get_user_model() 照常返回 django.contrib.auth.models.User,这个键只是躺在 settings 上没人读。

一个能跑的现代最小工程

  • 下面这套是用 5.2.19 真跑过的(2026-11-04 抓的输出),逐条对应上面的现代写法。

    1
    2
    3
    django-admin startproject mysite
    cd mysite
    python manage.py startapp blog
  • mysite/settings.py 里加应用:

    settings.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ALLOWED_HOSTS = ["127.0.0.1", "localhost"]

    INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "rest_framework",
    "blog",
    ]
  • blog/models.py:外键必须给 on_delete,信号不再写 providing_args。

    models.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    from django.conf import settings
    from django.db import models
    from django.dispatch import Signal

    article_published = Signal() # 5.2 已经没有 providing_args 这个参数


    class Grade(models.Model):
    name = models.CharField(max_length=32)

    def __str__(self): # 不再需要 @python_2_unicode_compatible
    return self.name


    class Student(models.Model):
    name = models.CharField(max_length=32)
    grade = models.ForeignKey(Grade, on_delete=models.CASCADE)
    owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="students",
    on_delete=models.CASCADE)
  • blog/urls.py:命名空间写在 app_name,不再写在 include() 里。

    urls.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    from django.urls import re_path

    from . import views

    app_name = "sunck" # 命名空间来自这里

    urlpatterns = [
    re_path(r"^students/(?P<pk>\d+)/$", views.grade_students, name="t3test"),
    re_path(r"^fired/$", views.fired, name="fired"),
    ]
  • mysite/urls.py:path() 管简单路由,re_path() 管正则。

    urls.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    from django.contrib import admin
    from django.urls import include, path, re_path

    from blog import views

    urlpatterns = [
    path("admin/", admin.site.urls),
    path("", views.index, name="index"),
    re_path(r"^", include("blog.urls")), # 命名空间走 blog/urls.py 的 app_name
    path("api-auth/", include("rest_framework.urls")), # DRF 自己已带 app_name
    ]
  • blog/views.py:is_ajax() 没了,自己看请求头。

    views.py
    1
    2
    3
    4
    5
    6
    7
    8
    from django.http import HttpResponse, JsonResponse

    from .models import Student, article_published


    def index(request):
    ajax = request.headers.get("x-requested-with") == "XMLHttpRequest"
    return JsonResponse({"ajax": ajax, "students": Student.objects.count()})
  • 建库、检查、跑起来,实测输出:

    1
    2
    3
    4
    python manage.py makemigrations blog
    python manage.py migrate
    python manage.py check
    python manage.py runserver 127.0.0.1:8431 --noreload
    1
    2
    3
    4
    5
    6
    Migrations for 'blog':
    blog\migrations\0001_initial.py
    + Create model Grade
    + Create model Student
    Applying sessions.0001_initial... OK
    System check identified no issues (0 silenced).
    1
    2
    3
    4
    [04/Nov/2026 20:12:25] "GET / HTTP/1.1" 200 29
    [04/Nov/2026 20:12:25] "GET /students/7/ HTTP/1.1" 200 4
    [04/Nov/2026 20:12:25] "GET /fired/ HTTP/1.1" 200 11
    [04/Nov/2026 20:12:25] "GET /admin/login/ HTTP/1.1" 200 4160
    • 带 X-Requested-With: XMLHttpRequest 请求 / 回来的是 {"ajax": true, "students": 0},说明请求头判断这条路是对的。
    • 反向解析实测:reverse("sunck:t3test", kwargs={"pk": 5}) 得到 /students/5/;模板里 {% url 'sunck:fired' %} 得到 /fired/;{% static 'x.css' %} 得到 /static/x.css。

迁移自己项目时的检查顺序

  • 按这个顺序走,能把「升级后莫名不生效」的坑一次排掉:
    1. 先只升级 Django 本身,第三方包全部不动,跑 python manage.py check;
    2. 打开 -W error::DeprecationWarning 或把 LOGGING 里 py.warnings 打出来,看有多少弃用警告;
    3. 装任何会连带解析依赖的包之前(pip install django-celery-beat 这种),先看它的 Requires-Dist 里 Django 的上限,别让 pip 静默降级;
    4. 把 settings 里那些「老名字」逐个搜一遍:MIDDLEWARE_CLASSES、TEMPLATE_DIRS、TEMPLATE_CONTEXT_PROCESSORS、TEMPLATE_LOADERS、AUTH_PROFILE_MODULE —— 它们不会报错,只会静默失效;
    5. 路由层全量搜 url(、django.conf.urls、include(..., namespace=;模型层搜不带 on_delete 的 ForeignKey( / OneToOneField( / ManyToManyField((多对多不强制,但外键必须);
    6. 用脚本改 settings 的话,改完 assert 期望内容确实在文件里(引号风格会被本机的 black 改变,见上一节);
    7. 最后跑一遍真实请求(runserver + 首页 + 一个带命名空间的反查),确认不是「能 import 但跑不起来」。

参考