This is some heavy stuff for a high school assignment.
<PERSON> answer is really good, so this is more of a supplementary answer to clarify some of the points he made.
A formal language is a mathematical construction. Their use for programming languages is just one of many possible uses; in fact, linguist <PERSON> made significant contributions to the early theory of formal languages. He invented the Chomsky Hierarchy, which classifies formal languages into regular, context-free, etc. Formal languages are also applied in linguistics to describe the syntax of natural languages like English. Think of it like the real numbers: we can use the real numbers to describe both concrete things like the distance from Los Angeles to New York, and abstract things like the ratio of a circle's circumference to its diameter. Even though both of those things exist independently of the real numbers, the real numbers are a helpful system for describing them. Formal languages are a helpful system for describing both English and Python, because both have a similar structured format.
Formal languages are just manipulations of symbols; they don't say anything about what the symbols mean. Think of an algebra problem like $a + b + c = d$. That equation has no inherent meaning, but we can still manipulate the symbols according to the rules of algebra; for example, we can rewrite it as $a + b = d - c$, even though we have no idea what the symbols mean. A way of giving meaning to the symbols within a system is called the semantics (in both natural and programming languages). So we could interpret $a$, $b$, and $c$ as dollar amounts, for example, and then the equation has meaning.
Classically, a programming language will have two grammars: a lexical grammar and a syntactic grammar. The lexical grammar deals with characters such as letters, semicolons, braces, and parentheses. It's usually a regular grammar, so it can be expressed with regular expressions or a DFA or NFA. 904
(There are proofs in formal language theory showing the three are equivalent in power—meaning they accept the same set of languages.) The lexing phase of the compiler or interpreter is sort of a mini interpreter for the regular language grammar. It reads the rules of the grammar, and following those rules, it lumps up individual characters into tokens. For example, if the language has an if statement that looks more or less like C's, the lexer might lump the characters i and f into the single token IF, then look for an opening parenthesis and output a token OPEN_PAREN, then handle whatever's between the parentheses, and then find the closing parenthesis and output a CLOSE_PAREN. When the lexer's done making tokens, it hands them over the parser, which determines if the tokens actually form valid statements of the programming language. So if you write ip a == b in Python, the lexer just does its best to guess what kind of token ip is (probably it would be taken for an identifier by most lexers), and passes it to the parser, which complains because you can't have an identifier in that position.
The parser implements the syntactic grammar, which is usually context-free, although as <PERSON>'s answer mentions, most programming languages nowadays actually don't use the full abilities of context-free grammars to make parsing simpler and more efficient. Here's Python's syntactic grammar, written in a variant of Backus-Naur form, which is a slight variant of the $a \rightarrow b$ format in the OP. The Java language specification also has examples of Java's lexical and syntactic grammars.
Let's look at the grammar rules for Python's if statement. This is the rule:
if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite]
That rule tells us how the parser will figure out if a string of tokens sent from the lexer is an if-statement. Any word in single quotes needs to appear, just like that, in the source code, so the parser will look for the plain word if. The parser will then try to match some tokens to the rule for test:
test: or_test ['if' or_test 'else' test] | lambdef
test is defined in terms of other rules in the grammar. Notice how test also includes itself in its definition; that's called a recursive definition. It's the big power of context-free languages that regular languages don't have, and it allows things like nested loops to be defined for programming language syntax.
If the parser manages to match some tokens to test, it will try to match a colon. If that succeeds, it will try to match some more tokens using the rule for suite. 904