Posting own answer which mostly works as I'd like (needed some special consideration for
While it works well for basic usage, errors will also replace the selection,
ideally a nonzero exit code would display as a message instead of putting the error into the editor.
;; See: https://emacs.stackexchange.com/a/34900/2418
;; Wrapper for 'shell-command-on-region', keeps the selection.
(start end command &optional
"Wrapper for 'shell-command-on-region', re-selecting the output.
Useful when called with a selection, so it can be modified in-place"
(let ((buffer-size-init (buffer-size)))
start end command output-buffer replace
(setq deactivate-mark nil)
(setq end (+ end (- (buffer-size) buffer-size-init)))
;; needed for evil line mode
(when (string= evil-state "visual")
(when (eq (evil-visual-type) evil-visual-line)
(evil-visual-select start end 'line)))))
(defun eval-region-as-py ()
"Evaluate selection as a python expression, replacing it with the result"
"python -c 'import sys; sys.stdout.write(str((eval(sys.stdin.read()))))'" 0 t))
(define-key evil-visual-state-map (kbd "RET") 'eval-region-as-py)