A slightly easier way to explain might be the fully-written-out equivalents of the IN clauses.
I've taken the liberty of "paraphrasing" T-SQL Syntax a little here, as there is no way of representing boolean values (the results of boolean operations) in T-SQL (or any SQL?) that I know of.
IF(NULL IN (34, 35, NULL))PRINT 'TRUE' =>
IF(NULL = 34 OR NULL = 35 OR NULL = NULL) PRINT 'TRUE' =>
IF(UNKNOWN OR UNKNOWN OR UNKNOWN) PRINT 'TRUE' =>
IF(UNKNOWN) PRINT 'TRUE'
and
IF(34 NOT IN (35, 36, NULL))PRINT 'FALSE' =>
IF(NOT (34 = 35 OR 34 = 36 OR 34 = NULL)) PRINT 'FALSE' =>
IF(NOT (FALSE OR FALSE OR UNKNOWN)) PRINT 'FALSE' =>
IF(NOT (UNKNOWN)) PRINT 'FALSE'
Because we all know(? 🙂 ) that "False OR UNKNOWN" evaluates to UNKNOWN, whereas "True OR UNKNOWN" evaluates to True...
There's some great articles on SQLServerCentral.com about Nulls, eg: http://www.sqlservercentral.com/articles/Advanced+Querying/2829/
Now if you REALLY want to have some fun, turn off the ANSI_NULLS option:
SET ANSI_NULLS OFF
All of a sudden three-valued logic becomes two-valued, and you have the equivalent of:
IF(NULL IN (34, 35, NULL))PRINT 'TRUE' =>
IF(NULL = 34 OR NULL = 35 OR NULL = NULL) PRINT 'TRUE' =>
IF(FALSE OR FALSE OR TRUE) PRINT 'TRUE' =>
IF(TRUE) PRINT 'TRUE'
and
IF(34 NOT IN (35, 36, NULL))PRINT 'FALSE' =>
IF(NOT (34 = 35 OR 34 = 36 OR 34 = NULL)) PRINT 'FALSE' =>
IF(NOT (FALSE OR FALSE OR FALSE)) PRINT 'FALSE' =>
IF(NOT (FALSE)) PRINT 'FALSE'
And now everything prints as you might have expected if you came to SQL Server before 2000, which is when ANSI_NULLS became default...
(and incidentally, that's a small issue in the question, which assumed that ANSI_NULLS was off - a reasonable assumption in most environments, but still a rich source of horrible confusion when it is not)
http://poorsql.com for T-SQL formatting: free as in speech, free as in beer, free to run in SSMS or on your version control server - free however you want it.