When outputting a static string to the page, is it necessary to escape the output?

The _e() function displays a translated string; so 1) You’re actually echoing a dynamic text; and 2) Yes, you should escape a translated string.

Relevant excerpt taken from the internationalization security guide in the Plugin Handbook:

Escape Internationalized Strings

You can’t trust that a translator will only add benign text to their localization; if they want to, they could add malicious JavaScript or
other code instead.
To protect against that, it’s important to
treat internationalized strings like you would any other untrusted input.

If you’re outputting the strings, then they should be escaped.

Insecure:

<?php _e( 'The REST API content endpoints were added in WordPress 4.7.', 'your-text-domain' ); ?>

Secure:

<?php esc_html_e( 'The REST API content endpoints were added in WordPress 4.7.', 'your-text-domain' ); ?>

In response to your comment:

example of when I might use _e() instead of esc_html_e()

So based on the excerpt from the internationalization security guide, I believe we should just use esc_html_e() and avoid using _e() unless we are 100% certain that a translation is completely clean from malicious code and special characters (apart from the basic ones like dots/., hypens/- and spaces).

And one may want to use _e() because he/she wants HTML code (e.g. b, a, etc.) in the translation to be interpreted (e.g. <b>hey</b> would visually show hey in bold like so: hey):

// if the translation was '<b>Enviar</b>' (in Spanish), then 'Enviar' would
// visually be in bold
_e( 'Submit' );
// equivalent to echo __( 'Submit' );

// but here, the text would remain as-is ('<b>Enviar</b>')
esc_html_e( 'Submit' );
// equivalent to echo esc_html( __( 'Submit' ) );

But then, instead of using _e(), one should use __() and with functions like wp_kses_data(), wp_kses_post() or wp_kses() which allows us to control the list of allowed HTML tags and attributes (e.g. we can allow/disallow href, onclick, etc.). And despite these functions do not guarantee that the output is actually secure, using them is at least better compared to simply echoing the raw HTML:

// what if the translation was 'Enviar <script>some bad JS code</script>' ?
_e( 'Submit' );

// wp_kses_data() by default disallows/removes <script> and </script> tags.
// sample output: 'Enviar some bad JS code' - doesn't look good.. but better
// than the browser executing the bad JS script.
echo wp_kses_data( __( 'Submit' ) );

Nevertheless, if one can ensure that a translation is secure (e.g. by moderating a translation), then using _e() would not be a problem — and in fact, it’s simpler (just one single function call)…

Resources

Notes

  • The WordPress core also calls _e() without escaping the HTML output… e.g. _e( 'Enter your password to view comments.' ); and that distracted my focus in writing the previous versions of this answer. Nonetheless, I’m not going to comment further on that possibly insecure _e() calls..

  • And just so you know, I’m not a security expert. 🙂