The main problems are:
\tl_if_blank:nTF expects a braced “normal argument” (n type) directly containing the tokens to test, such as \tl_if_blank:nTF { abc~def } { true } { false } or, if we are in a macro definition and argument #1 is a token list, \tl_if_blank:nTF {#1} { true } { false }. But what you want to test here is the contents (value) of a token list variable. For this, you need \tl_if_blank:VTF (the V causes the value of the first argument to be passed to the base form \tl_if_blank:nTF). Example:
\tl_if_blank:VTF \l__my_var_tl { true } { false }
When your \myQFormat has been fully executed, its \group_end: has restored the two token list variables \l__Qoptions_label_tl and \l__Qoptions_sublabel_tl to the values they had before the group started, i.e., empty here. So, when \question uses these variables as part of the question format, they are both empty.
For the second point, the following code calls \keys_set:nn inside the argument of \qformat. This allows you to use the names \l__Qoptions_label_tl and \l__Qoptions_sublabel_tl in the format definition. I'll show another possible approach below.
As egreg noted, the \tl_if_blank:... test for the label is not really needed, since the initial value of \l__Qoptions_label_tl is set with label .initial:n = { Question } (the braces may be omitted since there is no comma in Question). The only case where this test would be useful is if you want to obtain “Question” as the label when using the option label= or even label={ } (indeed, “blank” means ”empty or spaces only” in this context, therefore if you pass an argument containing only space tokens, \tl_if_blank:nTF will execute the “true” branch).
\documentclass[addpoints]{exam}
\usepackage{xparse}
\usepackage[xparse]{tcolorbox}
\renewcommand{\questionshook}{%
\setlength{\leftmargin}{0pt}%
\setlength{\labelwidth}{-\labelsep}%
}
\NewTColorBox{MarksTCBox} { O{} }{
left skip= 0pt,
right skip=0pt,
left=2pt,
right=2pt,
capture=hbox,
halign=center,
valign=center,
boxrule=0pt,
arc=0pt,
top=2pt,
bottom=2pt,
boxsep=0pt,
nobeforeafter,
box align = base,
baseline=4pt,
#1,
}
\ExplSyntaxOn
\keys_define:nn { Qoptions }
{
label .tl_set:N = \l__Qoptions_label_tl,
label .initial:n = { Question },
sublabel .tl_set:N = \l__Qoptions_sublabel_tl,
}
\cs_new_protected:Npn \Qoptions_question_header:n #1
{
\qformat
{
% \question appears to create a group, so the options are duly cleared
% when the question title has been typeset.
\keys_set:nn { Qoptions } {#1}
\textbf
{
\underline
{
\large
% Possible but not really needed (see above):
% \tl_if_blank:VTF \l__Qoptions_label_tl { Question }
% { \l__Qoptions_label_tl }
\tl_use:N \l__Qoptions_label_tl
\nobreakspace \thequestion \
\tl_if_blank:VF \l__Qoptions_sublabel_tl
{ [ \l__Qoptions_sublabel_tl ] \ }
\begin{MarksTCBox}
\scan_stop: [\totalpoints\ Marks]
\end{MarksTCBox}
}
}
\hfill % Otherwise, you'll have an Underfull \hbox for each question.
}
}
\NewDocumentCommand { \myQFormat } { O{} }
{
\Qoptions_question_header:n {#1}
}
\ExplSyntaxOff
\begin{document}
\begin{questions}
\myQFormat
\question[15]\hspace*{0pt}\vspace*{\baselineskip}
The output should be ``Question 1 [15 Marks]''
\myQFormat[label = Part]
\question[10]\hspace*{0pt}\vspace*{\baselineskip}
The output should be ``Part 2 [10 Marks]''
\myQFormat[label=Part, sublabel=Subtitle]
\question[5]\hspace*{0pt}\vspace*{\baselineskip}
The output should be ``Part 3 [Subtitle] [5 Marks]''
\myQFormat[sublabel=Subtitle]
\question[10]\hspace*{0pt}\vspace*{\baselineskip}
The output should be ``Question 4 [Subtitle] [10 Marks]''
\end{questions}
\end{document}

Other approach
Another way to avoid the problem due to the values of \l__Qoptions_label_tl and \l__Qoptions_sublabel_tl being restored too early (before \question uses them) is as follows. You can pass the values of \l__Qoptions_label_tl and \l__Qoptions_sublabel_tl to \Qoptions_question_header:n using a V argument type. Since you have two “normal arguments” to pass, the function now has to be named \Qoptions_question_header:nn and accept two arguments #1 and #2 (the label and sublabel). You can then use \cs_generate_variant:Nn \Qoptions_question_header:nn { VV } to create the needed function variant (namely, \Qoptions_question_header:VV), and call \Qoptions_question_header:VV \l__Qoptions_label_tl \l__Qoptions_sublabel_tl in \myQFormat to pass the values of \l__Qoptions_label_tl and \l__Qoptions_sublabel_tl to \Qoptions_question_header:nn.
\documentclass[addpoints]{exam}
\usepackage{xparse}
\usepackage[xparse]{tcolorbox}
\renewcommand{\questionshook}{%
\setlength{\leftmargin}{0pt}%
\setlength{\labelwidth}{-\labelsep}%
}
\NewTColorBox{MarksTCBox} { O{} }{
left skip= 0pt,
right skip=0pt,
left=2pt,
right=2pt,
capture=hbox,
halign=center,
valign=center,
boxrule=0pt,
arc=0pt,
top=2pt,
bottom=2pt,
boxsep=0pt,
nobeforeafter,
box align = base,
baseline=4pt,
#1,
}
\ExplSyntaxOn
\keys_define:nn { Qoptions }
{
label .tl_set:N = \l__Qoptions_label_tl,
label .initial:n = { Question },
sublabel .tl_set:N = \l__Qoptions_sublabel_tl,
}
\cs_new_protected:Npn \Qoptions_question_header:nn #1#2
{
\qformat
{
\textbf
{
\underline
{
\large
% Possible but not really needed (see above):
% \tl_if_blank:nTF {#1} { Question } {#1}
#1 \nobreakspace \thequestion \
\tl_if_blank:nF {#2} { [ #2 ] \ }
\begin{MarksTCBox}
\scan_stop: [\totalpoints\ Marks]
\end{MarksTCBox}
}
}
\hfill % Otherwise, you'll have an Underfull \hbox for each question.
}
}
\cs_generate_variant:Nn \Qoptions_question_header:nn { VV }
\NewDocumentCommand { \myQFormat } { O{} }
{
\group_begin:
\keys_set:nn { Qoptions } {#1}
\Qoptions_question_header:VV \l__Qoptions_label_tl \l__Qoptions_sublabel_tl
\group_end:
}
\ExplSyntaxOff
\begin{document}
\begin{questions}
\myQFormat
\question[15]\hspace*{0pt}\vspace*{\baselineskip}
The output should be ``Question 1 [15 Marks]''
\myQFormat[label = Part]
\question[10]\hspace*{0pt}\vspace*{\baselineskip}
The output should be ``Part 2 [10 Marks]''
\myQFormat[label=Part, sublabel=Subtitle]
\question[5]\hspace*{0pt}\vspace*{\baselineskip}
The output should be ``Part 3 [Subtitle] [5 Marks]''
\myQFormat[sublabel=Subtitle]
\question[10]\hspace*{0pt}\vspace*{\baselineskip}
The output should be ``Question 4 [Subtitle] [10 Marks]''
\end{questions}
\end{document}
Same output as above.
1-does setting an initial value oflabelmake sense or redundant when I have this line\tl_if_blank:VTF \l__Qoptions_label_tl { Question } { \l__Qoptions_label_tl }?2-what does\cs_new_protected:Npnmean? I mean its description in words like this is a function that accepts one input braced argument and uses it internally without braces. I tried to take a look at expl3 manual but it is not that friendly. – Diaa May 10 '20 at 12:27label .initial:n = { Question }withlabel .initial:n = { XXX }and you'll see the difference. This defines\l__Qoptions_label_tlat the earliest place (when\keys_define:nn { Qoptions } {...}is executed). It is used when thelabeloption is not passed to\myQFormat. 2)\cs_new_protected:Npnis similar to\protected\long\def, but inexpl3style, and errors out if the function is already defined. It is one of the ways to define a code-level function (as opposed to document-level ones defined withxparse's\NewDocumentCommandand friends).pin\cs_new_protected:Npnmeans we must write the parameter text after the function name (e.g.,#1#2in the second approach), just as with\def. It's simple work and makes reading the definition by TeX slightly faster than with\cs_new_protected:Nn. Theprotectedmakes it so that if the function your are defining (such as\Qoptions_question_header:nor\Qoptions_question_header:nnhere) is used in an expansion-only context (e.g.,\edef\x{\Qoptions_question_header:n{...}}or\write\somestream{\Qoptions_question_header:n{...}}, – frougon May 10 '20 at 12:52xande-type arguments inexpl3), it won't expand. This is useful because\Qoptions_question_header:nand\Qoptions_question_header:nncontain several “unexpandable macros” that are only useful to expand in a typesetting context (\textbfis one of them). In general, you should define macros as\protectedunless you know they will behave properly in an expansion-only context—this is whatxparsedoes with\NewDocumentCommandand friends, as opposed to\NewExpandableDocumentCommandand friends. – frougon May 10 '20 at 12:52:). – Diaa May 10 '20 at 12:55protectedthing, you probably need to search the site for “fully expandable” and “protected macros”. It's probably too difficult to make it clear in two or three comments (well, depending on what you already know). But if it's too complicated, just always useprotecteduntil you stumble on a problem which makes you realize that you don't want it. An example where you don't want it is if you define a macro that is used in anx-type argument (see below). – frougon May 10 '20 at 13:12\tl_set:Nx \l_tmpa_tl { Bla \Qoptions_foo:n {A} bla... }
– frougon May 10 '20 at 13:12. Here,\tl_set:Nxuses\Qoptions_foo:ninside\edefbehind the scenes for thex-type argument, and in order for\Qoptions_foo:nto expand in this context (inside\edef), it must be a non-\protected` macro.\protectedmacros and the meaning of “fully expandable,” I'd suggest reading On unprotecting (expanding) \protected macros (or, “the space after command name”) and Advantages and disadvantages of fully expandable macros. – frougon May 10 '20 at 13:18\tl_if_blank:...test for the label (little simplification noticed by egreg). – frougon May 10 '20 at 14:13