There is one rule for printing values in a template: wrap it at the point of output, with the function that matches that context. That is why an unescaped echo gets sent back in review under the WordPress coding standards.
Why output rather than input
“Could I not just clean it once when saving?” is a fair question. The answer is that the same value goes to several places. A post title alone appears as body text, as a title attribute, and inside JSON for a search response. Each of those defines safety differently.
Escaping at input means picking one context and freezing it, which leaves every other context wrong. It also corrupts the stored value, so searching, sorting and exporting all start behaving oddly. Escape at output and the original stays intact while each destination gets the treatment it needs.
The practical benefit is just as large. When the escaping sits on the same line as the echo, one line is enough to judge whether it is safe. If you have to trace back to the save path to be sure, that is not review — that is archaeology.
Function by context
<a class="<?php echo esc_attr( $class ); ?>"
href="<?php echo esc_url( $url ); ?>">
<?php echo esc_html( $title ); ?>
</a>
<div class="entry">
<?php echo wp_kses_post( $rich_html ); ?>
</div>
esc_html() and esc_attr() are separate because quotes are the escape character inside an attribute. A quote in the value terminates the attribute early and everything after it is parsed as new attributes. A character that is harmless outside a tag is dangerous inside one.
esc_url() does more than escape: it also checks whether the protocol is allowed. A user-supplied address can be a form that executes script, and esc_attr() will not catch that. Link positions always take esc_url().
wp_kses_post() is a different animal. It is not escaping but allow-list filtering, so paragraphs, links and emphasis survive while scripts are stripped. Use it only where the markup has to stay, such as editor-authored content.
Three common misuses
The first line is visible, so it gets caught quickly. The trouble is the second and third — the page looks perfectly fine, so they pass review. Having used an escaping function is not, by itself, a guarantee of anything.
When translation is involved, use the combined helpers such as esc_html__() and esc_attr_e(). Translated strings need the same treatment, and translation files are not always under your control.
Finally, distinguish esc_url() from esc_url_raw(). The former is for printing into HTML; the latter is for storage and redirects, where a machine consumes the value. The HTML variant converts & into an entity, so storing its result changes the data.
Security practice for theme and plugin code is covered further in the Security archive, and a before-and-after vulnerability assessment of a live site is part of our optimization program.
Next part
With output safe, the question becomes what to output. The next part covers secondary queries inside templates, and the deeply confusing symptom produced by forgetting a single line.