unique_ptr basic implementation for single objects
Don't mark your class final without a good reason. It inhibits user-freedom.
The default-access for members of a class is already private.
Explicitly deleting copy-constructor and copy-assignment-operator is superfluous, as you define move-constructor and move-assignment, which already suppresses them. Still, some assert being explicit adds clarity.
I wonder why you didn't declare construction from T* to be constexpr...
Try to consistently use the injected class-name (unique_ptr), instead of sporadically naming the template-arguments (unique_ptr&lt;T&gt;).
You are missing implicit upcasting in the move-ctor and move-assignment-operator.

template &lt;class U, class = std::enable_if_t&lt;
    std::has_virtual_destructor&lt;T&gt;() &amp;&amp; std::is_convertible&lt;U*, T*&gt;()&gt;&gt;
unique_ptr(unique_ptr&lt;U&gt;&amp;&amp; other) noexcept
: ptr_(other.release())
{}

template &lt;class U&gt;
auto operator=(std::unique_ptr&lt;U&gt;&amp;&amp; other) noexcept
-&gt; decltype((*this = unique_ptr(other))) {
    return *this = unique_ptr(other);
}

Take a look at std::exchange(object, value) from &lt;utility&gt;. It allows you to simplify some of your code.
If you use move-and-swap, you could isolate freeing of the referee to the dtor. Having it at only one place ensures you always do it the same, and is a good first step for retrofitting custom deleters. Not to mention that it in many cases simplifies the implementation.
(ptr != nullptr) can be simplified to ptr. In contexts where you have to force the type, !!ptr.
Why are op==() and op!=() inline-friend-functions, but swap() isn't? That's inconsistent. It's especially puzzling as they are all written to use the public interface only.
There is exactly one place where you don't have a single empty line between two functions, but two. Yes, that's nothing big.