Django Basics #2: Project Setup — uv + django-admin startproject
In #1 What is Django you nailed down where Django fits — full-stack. This post takes a single breath from empty directory → a working first page. The tool is the same uv you used in Modern Python FastAPI #1, kept consistent.
Prerequisites — Python and uv #
Django 5.x requires Python 3.10 or higher. This series runs on 3.13. Skip ahead if you already have uv; otherwise:
curl -LsSf https://astral.sh/uv/install.sh | shpowershell -c "irm https://astral.sh/uv/install.ps1 | iex"Creating the project #
Make a uv virtual environment in an empty directory and install Django.
mkdir myblog && cd myblog
uv init --python 3.13
uv add djangoWhat uv init creates:
pyproject.toml— project metadata and dependencies.python-version— the Python version to use.venv/— virtual environment (auto-created, excluded from git)
After uv add django lands, check the version:
uv run django-admin --version
# something like 5.1.xdjango-admin startproject — generate the project skeleton
#
Django has two CLI entry points — django-admin (before the project exists) and manage.py (after). The first skeleton is built with django-admin.
uv run django-admin startproject config .The trailing . matters. Without it, you end up nested like config/config/.... Adding . puts it directly into the current directory.
Why name it config? Names like mysite or myproject carry no meaning. It’s better to expose the essence: a collection of settings. It’s a community convention.
The structure that gets created #
myblog/
├── .python-version
├── .venv/
├── pyproject.toml
├── uv.lock
├── manage.py # Django CLI entry point (every command from here on)
└── config/
├── __init__.py
├── settings.py # all settings
├── urls.py # top-level URL routing
├── asgi.py # ASGI entry point (async, Channels)
└── wsgi.py # WSGI entry point (traditional sync deployment)What each file does:
manage.py— the entry for every command:runserver,migrate,createsuperuser, etc.settings.py— DB, app list, middleware, secret key, and so onurls.py— top-level URL mapping like/admin/,/posts/asgi.py/wsgi.py— server entry points. You don’t touch these during development
First run — runserver
#
uv run python manage.py runserverIt boots on the default port 8000. Open http://127.0.0.1:8000 in a browser and you’ll see a rocket page that reads “The install worked successfully! Congratulations!”
On the first run, you may see a red warning in the console like You have N unapplied migrations. The next step takes care of it.
Change the port:
uv run python manage.py runserver 8080Expose to the LAN:uv run python manage.py runserver 0.0.0.0:8000
First migration — tables for the built-in apps #
Django activates internal apps as soon as it’s installed (auth, sessions, admin, contenttypes, messages, staticfiles). Their tables (auth_user, django_session, etc.) need to be created in the DB.
uv run python manage.py migrateThe default DB is SQLite, and a db.sqlite3 file is auto-created at the project root. Switching to PostgreSQL or MySQL is covered in Intermediate #6.
Once migration finishes, the console fills with logs like Applying auth.0001_initial... OK, and the red warning goes away.
First app — startapp blog
#
In Django, an app is a reusable unit of functionality. Multiple apps inside one project — for example, blog, accounts, payments.
uv run python manage.py startapp blogThe structure that’s generated:
blog/
├── __init__.py
├── admin.py # Admin registration (#7)
├── apps.py # app metadata
├── migrations/
│ └── __init__.py
├── models.py # model definitions (#3)
├── tests.py # tests
└── views.py # views (#4)(The templates / static / urls directories are not auto-created — you add them yourself, as covered in #4 and #5.)
Register in INSTALLED_APPS
#
For Django to notice the app you made, register it in settings.py.
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"blog", # added
]A single "blog" line at the end of the list. More precisely, you can write "blog.apps.BlogConfig" (the class in apps.py), but the short "blog" form works.
settings.py core settings
#
Items worth touching first:
# Security — never expose externally
SECRET_KEY = "django-insecure-..."
# Debug — must be False in production
DEBUG = True
# Which hosts to allow when DEBUG=False
ALLOWED_HOSTS = []
# Installed apps
INSTALLED_APPS = [...]
# Middleware (request/response pipeline)
MIDDLEWARE = [...]
# Top-level URL module
ROOT_URLCONF = "config.urls"
# Template settings
TEMPLATES = [...]
# Database — default SQLite
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
# Internationalization
LANGUAGE_CODE = "ko-kr" # default en-us → Korean
TIME_ZONE = "Asia/Seoul" # UTC → Seoul
USE_I18N = True
USE_TZ = True # store as UTC in DB, convert when displaying
# Static files (#5)
STATIC_URL = "static/"DEBUG=True is for development only. Push it to production as-is and your error pages expose code and environment variables. Production deployment is in Advanced #7.
Separating environment variables — django-environ
#
Just like you used pydantic-settings in FastAPI #1, separating secrets into environment variables is the standard for Django too.
uv add django-environimport environ
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
env = environ.Env(
DEBUG=(bool, False),
)
environ.Env.read_env(BASE_DIR / ".env")
SECRET_KEY = env("SECRET_KEY")
DEBUG = env("DEBUG")
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=["127.0.0.1", "localhost"])SECRET_KEY=django-insecure-replace-me-in-production
DEBUG=True
ALLOWED_HOSTS=127.0.0.1,localhostAdding .env to .gitignore is a must.
A first view — the starting point of the series #
The smallest possible view in blog/views.py:
from django.http import HttpResponse
def index(request):
return HttpResponse("Hello, blog!")Create blog/urls.py (Django doesn’t auto-create it, remember).
from django.urls import path
from . import views
app_name = "blog"
urlpatterns = [
path("", views.index, name="index"),
]Include the blog URLs from the top-level config/urls.py.
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("blog/", include("blog.urls")),
]Restart the server and open http://127.0.0.1:8000/blog/ — Hello, blog! appears.
More on URL / view patterns in #4.
Frequently used manage.py commands #
Commands worth memorizing:
| Command | Purpose |
|---|---|
runserver | Run the development server |
startapp <name> | Create a new app |
makemigrations | Model changes → migration files |
migrate | Migrations → apply to DB |
createsuperuser | Create an admin superuser (#7) |
shell | A Python REPL with Django context |
dbshell | Direct DB shell |
collectstatic | Collect static files (production, #5) |
check | Settings/model integrity check |
test | Run tests (Intermediate #7) |
uv run python manage.py shell is especially handy. Import models and play with the ORM directly.
Recap #
What this post nailed down:
uv init+uv add djangofor dependency separationdjango-admin startproject config .— project skeletonmanage.py— every command from here onrunserver— dev server with auto-reloadmigrate— create tables for the built-in appsstartapp blog— create the first app, register inINSTALLED_APPS- Core
settings.pyitems —DEBUG,INSTALLED_APPS,DATABASES,LANGUAGE_CODE,TIME_ZONE django-environto separate secrets,.envexcluded from git- A first view —
views.py+ per-appurls.py+ top-levelinclude
In the next post (#3 Models and ORM basics), you’ll define a Post model in blog/models.py, walk through the makemigrations → migrate flow, and play with Django ORM’s QuerySet for the first time.