0.3 Starlark Syntax Basics
By the time you reach this section, Bazel's filesystem model and label syntax have already taught you where things live and how Bazel refers to them. A BUILD.bazel file looks enough like Python to invite the wrong reading.
load("//tools:defs.bzl", "my_rule")
package(default_visibility = ["//app:__subpackages__"])
my_rule(
name = "server",
deps = [":lib"],
)
The beginner instinct is to read that top to bottom as a little program: import something, run some setup, execute a function call. Read it for its declarative meaning instead: which symbols enter scope, which package-wide defaults apply, which targets exist, and what graph relationships their attributes describe.
BUILD Files Are Code-Shaped, Not Program-Shaped
This is the first place where Bazel can feel stricter than expected. BUILD files have function-call syntax, named arguments, list literals, and a Python-like surface. Bazel deliberately limits the application logic you can write inside them.
The restriction is not aesthetic. It keeps BUILD-file evaluation predictable, tool-friendly, and easy to reason about. The more a BUILD file behaves like a data declaration with a small amount of syntax, the easier it is for Bazel to load packages in parallel, for tools to reformat or rewrite files automatically, and for humans to scan a package without reverse-engineering hidden control flow.
Instead of asking, "what does this line do now?", ask, "what target does this declaration create?", "what package policy does this header set?", and "what kind of dependency edge does this attribute describe?" Those questions match the declarative role of a BUILD file.
Three Questions To Ask In Any BUILD File
Almost every article in this section sharpens one of three reading questions.
- What vocabulary entered scope before the target declarations? That is the job of 0.3.5 Load Statements.
- What is the overall shape of the file, and are there package-wide defaults in the header? That starts in 0.3.1 Anatomy of a BUILD File and connects back to 0.2.6 package() Function.
- What targets exist here, and what roles do their attributes play? That is the core of 0.3.2 Rules and 0.3.3 Attributes & Semantic Roles.
Around those three questions sit the helper concepts that make real BUILD files readable rather than repetitive. 0.3.4 Globs explains file selection inside one package. 0.3.6 filegroup Rule explains how a file set gets a reusable target name. 0.3.7 Aliases explains label indirection. 0.3.8 select() Awareness teaches you to recognize configuration-dependent values without needing the full configuration model yet.
Together, the articles train one habit: reading a BUILD file structurally before reading it line by line.
Why The Language Stays Smaller Than Python
The absence of top-level loops, top-level if statements, arbitrary I/O, and general script-like behavior keeps the package description from turning into an opaque program. That keeps the graph legible and the language aligned with the job of the file: declare targets and the relationships between them, then let later phases decide what to build and execute.
This is also why "boring" BUILD files are often good BUILD files. Repetition is sometimes cheaper than abstraction when both tools and future readers need to understand a package quickly. Level 0 is intentionally about reading comprehension before authorship. You do not need to become clever in BUILD files yet. You need to become fluent in what they are saying.
How To Read The Section
There is a clean progression here.
Start with 0.3.1 Anatomy of a BUILD File to get the file-level shape right. Move to 0.3.2 Rules and 0.3.3 Attributes & Semantic Roles to understand what the declarations are made of. Then read 0.3.4 Globs, 0.3.5 Load Statements, 0.3.6 filegroup Rule, and 0.3.7 Aliases as recurring building blocks that keep showing up in real repositories. Treat 0.3.8 select() Awareness as a recognition article: you do not need the full theory yet, only enough to avoid misreading configuration-dependent data as ordinary control flow.
That sequence mirrors how BUILD files become legible in practice. First you stop reading them as scripts. Then you learn the core nouns and roles. Then you learn the recurring patterns that make one package look different from another without changing the underlying model.
When a BUILD file feels dense, do one structural pass before any detailed reading. First scan the load() block. Then look for package(). Then list the target names and rule kinds. Only after that read srcs, deps, data, args, glob(), or select(). The file almost always becomes easier once you separate its zones.
To see how a Starlark evaluation failure is reported, run the starlark-traceback-error snippet and inspect its misspelled method call.
BUILD files may look like scripts, but their job is to declare package contents, target kinds, and dependency relationships in a constrained language. Read the load() block, package defaults, target declarations, and attributes by those roles to see the graph they describe.
Sections in this chapter · 8
What a typical BUILD file looks like: load statements, package() defaults, rule calls. No imperative logic.
Rule kinds as schemas, and rule calls as target declarations.
srcs (compile inputs), deps (library dependencies), data (runtime files) — distinct roles.
File-pattern expansion inside one package for srcs, data, and filegroups.
Importing public symbols from .bzl files into a BUILD file.
Grouping files under a named target for reuse across packages.
Creating alternative names for targets.
Conditional logic in BUILD files — covered in Level 3.2.